From 2539a204b7168fa846e543eeb8eb3a1efcf7b1e0 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 20 Jan 2025 13:12:57 +0100 Subject: [PATCH] feat: add more mathematical operations (#986) Closes partially #977 ### Summary of Changes Add many common mathematical operations (e.g. square root, logarithm, sine) to the `Cell.math` namespace. --- .../tabular/query/_lazy_math_operations.py | 78 ++ .../data/tabular/query/_math_operations.py | 749 ++++++++++++++++++ .../_functional_table_transformer.py | 2 +- tests/helpers/_assertions.py | 19 +- .../query/_lazy_math_operations/test_acos.py | 25 + .../query/_lazy_math_operations/test_acosh.py | 28 + .../query/_lazy_math_operations/test_asin.py | 25 + .../query/_lazy_math_operations/test_asinh.py | 30 + .../query/_lazy_math_operations/test_atan.py | 25 + .../query/_lazy_math_operations/test_atanh.py | 30 + .../query/_lazy_math_operations/test_cbrt.py | 26 + .../query/_lazy_math_operations/test_cos.py | 29 + .../query/_lazy_math_operations/test_cosh.py | 30 + .../test_degrees_to_radians.py | 38 + .../query/_lazy_math_operations/test_exp.py | 23 + .../query/_lazy_math_operations/test_ln.py | 25 + .../query/_lazy_math_operations/test_log.py | 59 ++ .../query/_lazy_math_operations/test_log10.py | 27 + .../test_radians_to_degrees.py | 38 + .../test_round_to_decimal_places.py | 68 ++ .../test_round_to_significant_figures.py | 115 +++ .../query/_lazy_math_operations/test_sign.py | 29 + .../query/_lazy_math_operations/test_sin.py | 29 + .../query/_lazy_math_operations/test_sinh.py | 30 + .../query/_lazy_math_operations/test_sqrt.py | 28 + .../query/_lazy_math_operations/test_tan.py | 29 + .../query/_lazy_math_operations/test_tanh.py | 30 + 27 files changed, 1661 insertions(+), 3 deletions(-) create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_acos.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_acosh.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_asin.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_asinh.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_atan.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_atanh.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_cbrt.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_cos.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_cosh.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_degrees_to_radians.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_exp.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_ln.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_log.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_log10.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_radians_to_degrees.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_round_to_decimal_places.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_round_to_significant_figures.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_sign.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_sin.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_sinh.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_sqrt.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_tan.py create mode 100644 tests/safeds/data/tabular/query/_lazy_math_operations/test_tanh.py diff --git a/src/safeds/data/tabular/query/_lazy_math_operations.py b/src/safeds/data/tabular/query/_lazy_math_operations.py index b141fe143..acd5e4bf0 100644 --- a/src/safeds/data/tabular/query/_lazy_math_operations.py +++ b/src/safeds/data/tabular/query/_lazy_math_operations.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING from safeds._utils import _structural_hash +from safeds._validation import _check_bounds, _ClosedBound, _OpenBound from safeds.data.tabular.containers._lazy_cell import _LazyCell from safeds.data.tabular.query._math_operations import MathOperations @@ -46,8 +47,85 @@ def __str__(self) -> str: def abs(self) -> Cell: return _LazyCell(self._expression.__abs__()) + def acos(self) -> Cell: + return _LazyCell(self._expression.arccos()) + + def acosh(self) -> Cell: + return _LazyCell(self._expression.arccosh()) + + def asin(self) -> Cell: + return _LazyCell(self._expression.arcsin()) + + def asinh(self) -> Cell: + return _LazyCell(self._expression.arcsinh()) + + def atan(self) -> Cell: + return _LazyCell(self._expression.arctan()) + + def atanh(self) -> Cell: + return _LazyCell(self._expression.arctanh()) + + def cbrt(self) -> Cell: + return _LazyCell(self._expression.cbrt()) + def ceil(self) -> Cell: return _LazyCell(self._expression.ceil()) + def cos(self) -> Cell: + return _LazyCell(self._expression.cos()) + + def cosh(self) -> Cell: + return _LazyCell(self._expression.cosh()) + + def degrees_to_radians(self) -> Cell: + return _LazyCell(self._expression.radians()) + + def exp(self) -> Cell: + return _LazyCell(self._expression.exp()) + def floor(self) -> Cell: return _LazyCell(self._expression.floor()) + + def ln(self) -> Cell: + return _LazyCell(self._expression.log()) + + def log(self, base: float) -> Cell: + _check_bounds("base", base, lower_bound=_OpenBound(0)) + if base == 1: + raise ValueError("The base of the logarithm must not be 1.") + + return _LazyCell(self._expression.log(base)) + + def log10(self) -> Cell: + return _LazyCell(self._expression.log10()) + + def radians_to_degrees(self) -> Cell: + return _LazyCell(self._expression.degrees()) + + def round_to_decimal_places(self, decimal_places: int) -> Cell: + _check_bounds("decimal_places", decimal_places, lower_bound=_ClosedBound(0)) + + return _LazyCell(self._expression.round(decimal_places)) + + def round_to_significant_figures(self, significant_figures: int) -> Cell: + _check_bounds("significant_figures", significant_figures, lower_bound=_ClosedBound(1)) + + return _LazyCell(self._expression.round_sig_figs(significant_figures)) + + def sign(self) -> Cell: + return _LazyCell(self._expression.sign()) + + def sin(self) -> Cell: + return _LazyCell(self._expression.sin()) + + def sinh(self) -> Cell: + return _LazyCell(self._expression.sinh()) + + def sqrt(self) -> Cell: + return _LazyCell(self._expression.sqrt()) + + def tan(self) -> Cell: + return _LazyCell(self._expression.tan()) + + def tanh(self) -> Cell: + return _LazyCell(self._expression.tanh()) diff --git a/src/safeds/data/tabular/query/_math_operations.py b/src/safeds/data/tabular/query/_math_operations.py index 7f7b208a7..0c33ccd69 100644 --- a/src/safeds/data/tabular/query/_math_operations.py +++ b/src/safeds/data/tabular/query/_math_operations.py @@ -5,6 +5,7 @@ if TYPE_CHECKING: from safeds.data.tabular.containers import Cell + from safeds.exceptions import OutOfBoundsError # noqa: F401 class MathOperations(ABC): @@ -12,6 +13,21 @@ class MathOperations(ABC): Namespace for mathematical operations. This class cannot be instantiated directly. It can only be accessed using the `math` attribute of a cell. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1]) + >>> column.transform(lambda cell: cell.math.abs()) + +-----+ + | a | + | --- | + | i64 | + +=====+ + | 1 | + | 0 | + | 1 | + +-----+ """ # ------------------------------------------------------------------------------------------------------------------ @@ -42,6 +58,11 @@ def abs(self) -> Cell: """ Get the absolute value. + Returns + ------- + cell: + The absolute value. + Examples -------- >>> from safeds.data.tabular.containers import Column @@ -58,11 +79,204 @@ def abs(self) -> Cell: +------+ """ + @abstractmethod + def acos(self) -> Cell: + """ + Get the inverse cosine. + + Returns + ------- + cell: + The inverse cosine. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1, None]) + >>> column.transform(lambda cell: cell.math.acos()) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | 3.14159 | + | 1.57080 | + | 0.00000 | + | null | + +---------+ + """ + + @abstractmethod + def acosh(self) -> Cell: + """ + Get the inverse hyperbolic cosine. + + Returns + ------- + cell: + The inverse hyperbolic cosine. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1, None]) + >>> column.transform(lambda cell: cell.math.acosh()) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | NaN | + | NaN | + | 0.00000 | + | null | + +---------+ + """ + + @abstractmethod + def asin(self) -> Cell: + """ + Get the inverse sine. + + Returns + ------- + cell: + The inverse sine. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1, None]) + >>> column.transform(lambda cell: cell.math.asin()) + +----------+ + | a | + | --- | + | f64 | + +==========+ + | -1.57080 | + | 0.00000 | + | 1.57080 | + | null | + +----------+ + """ + + @abstractmethod + def asinh(self) -> Cell: + """ + Get the inverse hyperbolic sine. + + Returns + ------- + cell: + The inverse hyperbolic sine. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1, None]) + >>> column.transform(lambda cell: cell.math.asinh()) + +----------+ + | a | + | --- | + | f64 | + +==========+ + | -0.88137 | + | 0.00000 | + | 0.88137 | + | null | + +----------+ + """ + + @abstractmethod + def atan(self) -> Cell: + """ + Get the inverse tangent. + + Returns + ------- + cell: + The inverse tangent. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1, None]) + >>> column.transform(lambda cell: cell.math.atan()) + +----------+ + | a | + | --- | + | f64 | + +==========+ + | -0.78540 | + | 0.00000 | + | 0.78540 | + | null | + +----------+ + """ + + @abstractmethod + def atanh(self) -> Cell: + """ + Get the inverse hyperbolic tangent. + + Returns + ------- + cell: + The inverse hyperbolic tangent. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1, None]) + >>> column.transform(lambda cell: cell.math.atanh()) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | -inf | + | 0.00000 | + | inf | + | null | + +---------+ + """ + + @abstractmethod + def cbrt(self) -> Cell: + """ + Get the cube root. + + Returns + ------- + cell: + The cube root. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [1, 8, None]) + >>> column.transform(lambda cell: cell.math.cbrt()) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | 1.00000 | + | 2.00000 | + | null | + +---------+ + """ + @abstractmethod def ceil(self) -> Cell: """ Round up to the nearest integer. + Returns + ------- + cell: + The rounded value. + Examples -------- >>> from safeds.data.tabular.containers import Column @@ -79,11 +293,129 @@ def ceil(self) -> Cell: +---------+ """ + @abstractmethod + def cos(self) -> Cell: + """ + Get the cosine. + + Returns + ------- + cell: + The cosine. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [0, math.pi / 2, math.pi, 3 * math.pi / 2, None]) + >>> column.transform(lambda cell: cell.math.cos()) + +----------+ + | a | + | --- | + | f64 | + +==========+ + | 1.00000 | + | 0.00000 | + | -1.00000 | + | -0.00000 | + | null | + +----------+ + """ + + @abstractmethod + def cosh(self) -> Cell: + """ + Get the hyperbolic cosine. + + Returns + ------- + cell: + The hyperbolic cosine. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1, None]) + >>> column.transform(lambda cell: cell.math.cosh()) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | 1.54308 | + | 1.00000 | + | 1.54308 | + | null | + +---------+ + """ + + @abstractmethod + def degrees_to_radians(self) -> Cell: + """ + Convert degrees to radians. + + Returns + ------- + cell: + The value in radians. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [0, 90, 180, 270, None]) + >>> column.transform(lambda cell: cell.math.degrees_to_radians()) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | 0.00000 | + | 1.57080 | + | 3.14159 | + | 4.71239 | + | null | + +---------+ + """ + + @abstractmethod + def exp(self) -> Cell: + """ + Get the exponential. + + Returns + ------- + cell: + The exponential. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1, None]) + >>> column.transform(lambda cell: cell.math.exp()) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | 0.36788 | + | 1.00000 | + | 2.71828 | + | null | + +---------+ + """ + @abstractmethod def floor(self) -> Cell: """ Round down to the nearest integer. + Returns + ------- + cell: + The rounded value. + Examples -------- >>> from safeds.data.tabular.containers import Column @@ -99,3 +431,420 @@ def floor(self) -> Cell: | null | +---------+ """ + + @abstractmethod + def ln(self) -> Cell: + """ + Get the natural logarithm. + + Returns + ------- + cell: + The natural logarithm. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [0, 1, math.e, None]) + >>> column.transform(lambda cell: cell.math.ln()) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | -inf | + | 0.00000 | + | 1.00000 | + | null | + +---------+ + """ + + @abstractmethod + def log(self, base: float) -> Cell: + """ + Get the logarithm to the specified base. + + Parameters + ---------- + base: + The base of the logarithm. Must be positive and not equal to 1. + + Returns + ------- + cell: + The logarithm. + + Raises + ------ + ValueError + If the base is less than or equal to 0 or equal to 1. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column1 = Column("a", [0, 1, math.e, None]) + >>> column1.transform(lambda cell: cell.math.log(math.e)) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | -inf | + | 0.00000 | + | 1.00000 | + | null | + +---------+ + + >>> column2 = Column("a", [0, 1, 10, None]) + >>> column2.transform(lambda cell: cell.math.log(10)) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | -inf | + | 0.00000 | + | 1.00000 | + | null | + +---------+ + """ + + @abstractmethod + def log10(self) -> Cell: + """ + Get the common logarithm (base 10). + + Returns + ------- + cell: + The common logarithm. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [0, 1, 10, None]) + >>> column.transform(lambda cell: cell.math.log10()) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | -inf | + | 0.00000 | + | 1.00000 | + | null | + +---------+ + """ + + @abstractmethod + def radians_to_degrees(self) -> Cell: + """ + Convert radians to degrees. + + Returns + ------- + cell: + The value in degrees. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [0, math.pi / 2, math.pi, 3 * math.pi / 2, None]) + >>> column.transform(lambda cell: cell.math.radians_to_degrees()) + +-----------+ + | a | + | --- | + | f64 | + +===========+ + | 0.00000 | + | 90.00000 | + | 180.00000 | + | 270.00000 | + | null | + +-----------+ + """ + + @abstractmethod + def round_to_decimal_places(self, decimal_places: int) -> Cell: + """ + Round to the specified number of decimal places. + + Parameters + ---------- + decimal_places: + The number of decimal places to round to. Must be greater than or equal to 0. + + Returns + ------- + cell: + The rounded value. + + Raises + ------ + OutOfBoundsError + If `decimal_places` is less than 0. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [0.999, 1.123, 3.456, None]) + >>> column.transform(lambda cell: cell.math.round_to_decimal_places(0)) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | 1.00000 | + | 1.00000 | + | 3.00000 | + | null | + +---------+ + + >>> column.transform(lambda cell: cell.math.round_to_decimal_places(2)) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | 1.00000 | + | 1.12000 | + | 3.46000 | + | null | + +---------+ + """ + + @abstractmethod + def round_to_significant_figures(self, significant_figures: int) -> Cell: + """ + Round to the specified number of significant figures. + + Parameters + ---------- + significant_figures: + The number of significant figures to round to. Must be greater than or equal to 1. + + Returns + ------- + cell: + The rounded value. + + Raises + ------ + OutOfBoundsError + If `significant_figures` is less than 1. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [0.999, 1.123, 3.456, None]) + >>> column.transform(lambda cell: cell.math.round_to_significant_figures(1)) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | 1.00000 | + | 1.00000 | + | 3.00000 | + | null | + +---------+ + + >>> column.transform(lambda cell: cell.math.round_to_significant_figures(2)) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | 1.00000 | + | 1.10000 | + | 3.50000 | + | null | + +---------+ + """ + + @abstractmethod + def sign(self) -> Cell: + """ + Get the sign (-1 for negative numbers, 0 for zero, and 1 for positive numbers). + + Note that IEEE 754 defines a negative zero (-0) and a positive zero (+0). This method return a negative zero + for -0 and a positive zero for +0. + + Returns + ------- + cell: + The sign. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column1 = Column("a", [-1, 0, 1, None]) + >>> column1.transform(lambda cell: cell.math.sign()) + +------+ + | a | + | --- | + | i64 | + +======+ + | -1 | + | 0 | + | 1 | + | null | + +------+ + + >>> column2 = Column("a", [-1.0, -0.0, +0.0, 1.0, None]) + >>> column2.transform(lambda cell: cell.math.sign()) + +----------+ + | a | + | --- | + | f64 | + +==========+ + | -1.00000 | + | -0.00000 | + | 0.00000 | + | 1.00000 | + | null | + +----------+ + """ + + @abstractmethod + def sin(self) -> Cell: + """ + Get the sine. + + Returns + ------- + cell: + The sine. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [0, math.pi / 2, math.pi, 3 * math.pi / 2, None]) + >>> column.transform(lambda cell: cell.math.sin()) + +----------+ + | a | + | --- | + | f64 | + +==========+ + | 0.00000 | + | 1.00000 | + | 0.00000 | + | -1.00000 | + | null | + +----------+ + """ + + @abstractmethod + def sinh(self) -> Cell: + """ + Get the hyperbolic sine. + + Returns + ------- + cell: + The hyperbolic sine. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1, None]) + >>> column.transform(lambda cell: cell.math.sinh()) + +----------+ + | a | + | --- | + | f64 | + +==========+ + | -1.17520 | + | 0.00000 | + | 1.17520 | + | null | + +----------+ + """ + + @abstractmethod + def sqrt(self) -> Cell: + """ + Get the square root. + + Returns + ------- + cell: + The square root. + + Examples + -------- + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [1, 4, None]) + >>> column.transform(lambda cell: cell.math.sqrt()) + +---------+ + | a | + | --- | + | f64 | + +=========+ + | 1.00000 | + | 2.00000 | + | null | + +---------+ + """ + + @abstractmethod + def tan(self) -> Cell: + """ + Get the tangent. + + Returns + ------- + cell: + The tangent. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [0, math.pi / 4, 3 * math.pi / 4, None]) + >>> column.transform(lambda cell: cell.math.tan()) + +----------+ + | a | + | --- | + | f64 | + +==========+ + | 0.00000 | + | 1.00000 | + | -1.00000 | + | null | + +----------+ + """ + + @abstractmethod + def tanh(self) -> Cell: + """ + Get the hyperbolic tangent. + + Returns + ------- + cell: + The hyperbolic tangent. + + Examples + -------- + >>> import math + >>> from safeds.data.tabular.containers import Column + >>> column = Column("a", [-1, 0, 1, None]) + >>> column.transform(lambda cell: cell.math.tanh()) + +----------+ + | a | + | --- | + | f64 | + +==========+ + | -0.76159 | + | 0.00000 | + | 0.76159 | + | null | + +----------+ + """ diff --git a/src/safeds/data/tabular/transformation/_functional_table_transformer.py b/src/safeds/data/tabular/transformation/_functional_table_transformer.py index 7c4add23c..eed68c3b4 100644 --- a/src/safeds/data/tabular/transformation/_functional_table_transformer.py +++ b/src/safeds/data/tabular/transformation/_functional_table_transformer.py @@ -77,7 +77,7 @@ def transform(self, table: Table) -> Table: Parameters ---------- table: - The table on which on which the callable is executed. + The table on which the callable is executed. Returns ------- diff --git a/tests/helpers/_assertions.py b/tests/helpers/_assertions.py index 5846c28f9..046ed61b6 100644 --- a/tests/helpers/_assertions.py +++ b/tests/helpers/_assertions.py @@ -1,5 +1,6 @@ +import math from collections.abc import Callable -from typing import Any +from typing import Any, SupportsFloat from polars.testing import assert_frame_equal @@ -68,6 +69,7 @@ def assert_cell_operation_works( expected: Any, *, type_if_none: ColumnType | None = None, + ignore_float_imprecision: bool = True, ) -> None: """ Assert that a cell operation works as expected. @@ -82,12 +84,25 @@ def assert_cell_operation_works( The expected value of the transformed cell. type_if_none: The type of the column if the value is `None`. + ignore_float_imprecision: + If False, check if floating point values match EXACTLY. """ type_ = type_if_none if value is None else None column = Column("a", [value], type=type_) transformed_column = column.transform(transformer) actual = transformed_column[0] - assert actual == expected, f"Expected {expected}, but got {actual}." + + message = f"Expected {expected}, but got {actual}." + + if expected is None: + assert actual is None, message + elif isinstance(expected, SupportsFloat) and math.isnan(expected): + # NaN != NaN + assert math.isnan(actual), message + elif isinstance(expected, SupportsFloat) and ignore_float_imprecision: + assert math.isclose(actual, expected, abs_tol=1e-15), message + else: + assert actual == expected, message def assert_row_operation_works( diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_acos.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_acos.py new file mode 100644 index 000000000..c65f11bde --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_acos.py @@ -0,0 +1,25 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (1.0, 0), + (0, math.pi / 2), + (-1, math.pi), + (None, None), + ], + ids=[ + "0 deg", + "90 deg", + "180 deg", + "None", + ], +) +def test_should_return_inverse_cosine(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.acos(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_acosh.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_acosh.py new file mode 100644 index 000000000..d61a1b4a8 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_acosh.py @@ -0,0 +1,28 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + +E = math.e +PHI = (1 + math.sqrt(5)) / 2 + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (1, 0), + (0.5 * (E + 1 / E), 1), + (0.5 * math.sqrt(5), math.log(PHI)), + (None, None), + ], + ids=[ + "0", + "1", + "ln of golden ratio", + "None", + ], +) +def test_should_return_inverse_hyperbolic_cosine(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.acosh(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_asin.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_asin.py new file mode 100644 index 000000000..c2304c326 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_asin.py @@ -0,0 +1,25 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (-1.0, -math.pi / 2), + (0, 0), + (1, math.pi / 2), + (None, None), + ], + ids=[ + "-90 deg", + "0 deg", + "90 deg", + "None", + ], +) +def test_should_return_inverse_sine(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.asin(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_asinh.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_asinh.py new file mode 100644 index 000000000..39fde9be6 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_asinh.py @@ -0,0 +1,30 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + +E = math.e +PHI = (1 + math.sqrt(5)) / 2 + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 0), + (-0.5 * (E - 1 / E), -1), + (0.5 * (E - 1 / E), 1), + (0.5, math.log(PHI)), + (None, None), + ], + ids=[ + "0", + "-1", + "1", + "ln of golden ratio", + "None", + ], +) +def test_should_return_inverse_hyperbolic_sine(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.asinh(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_atan.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_atan.py new file mode 100644 index 000000000..e6078bbdc --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_atan.py @@ -0,0 +1,25 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (-1.0, -math.pi / 4), + (0, 0), + (1, math.pi / 4), + (None, None), + ], + ids=[ + "-45 deg", + "0 deg", + "45 deg", + "None", + ], +) +def test_should_return_inverse_tangent(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.atan(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_atanh.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_atanh.py new file mode 100644 index 000000000..5fd72b305 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_atanh.py @@ -0,0 +1,30 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + +E = math.e +PHI = (1 + math.sqrt(5)) / 2 + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 0), + (-(E - 1 / E) / (E + 1 / E), -1), + ((E - 1 / E) / (E + 1 / E), 1), + (1 / math.sqrt(5), math.log(PHI)), + (None, None), + ], + ids=[ + "0", + "-1", + "1", + "ln of golden ratio", + "None", + ], +) +def test_should_return_inverse_hyperbolic_tangent(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.atanh(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_cbrt.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_cbrt.py new file mode 100644 index 000000000..32b6b01e6 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_cbrt.py @@ -0,0 +1,26 @@ +import pytest + +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (-1, -1), + (0, 0), + (1, 1), + (3.375, 1.5), + (8, 2), + (None, None), + ], + ids=[ + "-1", + "0", + "1", + "cube of float", + "cube of int", + "None", + ], +) +def test_should_return_cube_root(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.cbrt(), expected) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_cos.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_cos.py new file mode 100644 index 000000000..0c3255f41 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_cos.py @@ -0,0 +1,29 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 1), + (math.pi / 2, 0), + (math.pi, -1), + (3 * math.pi / 2, 0), + (2 * math.pi, 1), + (None, None), + ], + ids=[ + "0 deg", + "90 deg", + "180 deg", + "270 deg", + "360 deg", + "None", + ], +) +def test_should_return_cosine(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.cos(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_cosh.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_cosh.py new file mode 100644 index 000000000..6843c0049 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_cosh.py @@ -0,0 +1,30 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + +E = math.e +PHI = (1 + math.sqrt(5)) / 2 + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 1), + (-1, 0.5 * (E + 1 / E)), + (1, 0.5 * (E + 1 / E)), + (math.log(PHI), 0.5 * math.sqrt(5)), + (None, None), + ], + ids=[ + "0", + "-1", + "1", + "ln of golden ratio", + "None", + ], +) +def test_should_return_hyperbolic_cosine(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.cosh(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_degrees_to_radians.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_degrees_to_radians.py new file mode 100644 index 000000000..5a44485fb --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_degrees_to_radians.py @@ -0,0 +1,38 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 0), + (22.5, math.pi / 8), + (90, math.pi / 2), + (180, math.pi), + (270, 3 * math.pi / 2), + (360, 2 * math.pi), + (720, 4 * math.pi), + (None, None), + ], + ids=[ + "0 deg", + "22.5 deg", + "90 deg", + "180 deg", + "270 deg", + "360 deg", + "720 deg", + "None", + ], +) +def test_should_convert_degrees_to_radians(value: float | None, expected: float | None) -> None: + assert_cell_operation_works( + value, + lambda cell: cell.math.degrees_to_radians(), + expected, + type_if_none=ColumnType.float64(), + ) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_exp.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_exp.py new file mode 100644 index 000000000..578868387 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_exp.py @@ -0,0 +1,23 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 1), + (1.0, math.e), + (None, None), + ], + ids=[ + "0", + "1", + "None", + ], +) +def test_should_return_exponential(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.exp(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_ln.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_ln.py new file mode 100644 index 000000000..63bc11424 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_ln.py @@ -0,0 +1,25 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, -math.inf), + (1, 0), + (math.e, 1), + (None, None), + ], + ids=[ + "0", + "1", + "e", + "None", + ], +) +def test_should_return_natural_logarithm(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.ln(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_log.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_log.py new file mode 100644 index 000000000..4ed6456eb --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_log.py @@ -0,0 +1,59 @@ +import math + +import pytest + +from safeds.data.tabular.containers import Column +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "base", "expected"), + [ + # base e + (0, math.e, -math.inf), + (1, math.e, 0), + (math.e, math.e, 1), + # base 10 + (0, 10, -math.inf), + (1, 10, 0), + (10, 10, 1), + (100, 10, 2), + # None + (None, 10, None), + ], + ids=[ + # base e + "base e - 0", + "base e - 1", + "base e - e", + # base 10 + "base 10 - 0", + "base 10 - 1", + "base 10 - 10", + "base 10 - 100", + # None + "None", + ], +) +def test_should_return_logarithm_to_given_base(value: float | None, base: int, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.log(base), expected, type_if_none=ColumnType.float64()) + + +@pytest.mark.parametrize( + "base", + [ + -1, + 0, + 1, + ], + ids=[ + "negative", + "zero", + "one", + ], +) +def test_should_raise_if_base_is_out_of_bounds(base: int) -> None: + column = Column("a", [1]) + with pytest.raises(ValueError, match="base"): + column.transform(lambda cell: cell.math.log(base)) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_log10.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_log10.py new file mode 100644 index 000000000..b784a9e21 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_log10.py @@ -0,0 +1,27 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, -math.inf), + (1, 0), + (10, 1), + (100, 2), + (None, None), + ], + ids=[ + "0", + "1", + "10", + "100", + "None", + ], +) +def test_should_return_common_logarithm(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.log10(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_radians_to_degrees.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_radians_to_degrees.py new file mode 100644 index 000000000..6ab5e615a --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_radians_to_degrees.py @@ -0,0 +1,38 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 0), + (math.pi / 8, 22.5), + (math.pi / 2, 90), + (math.pi, 180), + (3 * math.pi / 2, 270), + (2 * math.pi, 360), + (4 * math.pi, 720), + (None, None), + ], + ids=[ + "0 deg", + "22.5 deg", + "90 deg", + "180 deg", + "270 deg", + "360 deg", + "720 deg", + "None", + ], +) +def test_should_convert_radians_to_degrees(value: float | None, expected: float | None) -> None: + assert_cell_operation_works( + value, + lambda cell: cell.math.radians_to_degrees(), + expected, + type_if_none=ColumnType.float64(), + ) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_round_to_decimal_places.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_round_to_decimal_places.py new file mode 100644 index 000000000..7035adaf8 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_round_to_decimal_places.py @@ -0,0 +1,68 @@ +import pytest + +from safeds.data.tabular.containers import Column +from safeds.data.tabular.typing import ColumnType +from safeds.exceptions import OutOfBoundsError +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "decimal_places", "expected"), + [ + # Zero + (0, 1, 0), + (0.0, 1, 0), + # Zero decimal places + (0.1, 0, 0), + (1, 0, 1), + (1.1, 0, 1), + # Rounding down + (0.14, 1, 0.1), + (0.104, 2, 0.1), + # Rounding up + (0.15, 1, 0.2), + (0.105, 2, 0.11), + # Overflow + (9.99, 1, 10), + (9.99, 2, 9.99), + # None + (None, 1, None), + ], + ids=[ + # Zero + "0", + "0.0", + # Zero decimal places + "0.1 (0 decimal places)", + "1 (0 decimal places)", + "1.1 (0 decimal places)", + # Rounding down + "0.14 (1 decimal places)", + "0.104 (2 decimal places)", + # Rounding up + "0.15 (1 decimal places)", + "0.105 (2 decimal places)", + # Overflow + "9.99 (1 decimal places)", + "9.99 (2 decimal places)", + # None + "None", + ], +) +def test_should_round_to_decimal_places( + value: float | None, + decimal_places: int, + expected: float | None, +) -> None: + assert_cell_operation_works( + value, + lambda cell: cell.math.round_to_decimal_places(decimal_places), + expected, + type_if_none=ColumnType.float64(), + ) + + +def test_should_raise_if_decimal_places_is_out_of_bounds() -> None: + column = Column("a", [1]) + with pytest.raises(OutOfBoundsError): + column.transform(lambda cell: cell.math.round_to_decimal_places(-1)) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_round_to_significant_figures.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_round_to_significant_figures.py new file mode 100644 index 000000000..3947c5d9f --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_round_to_significant_figures.py @@ -0,0 +1,115 @@ +import pytest + +from safeds.data.tabular.containers import Column +from safeds.data.tabular.typing import ColumnType +from safeds.exceptions import OutOfBoundsError +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "significant_figures", "expected"), + [ + # Zero + (0, 1, 0), + (0.0, 1, 0), + # (0, 0.1) + (0.05, 1, 0.05), + (0.05, 2, 0.05), + (0.054, 1, 0.05), + (0.054, 2, 0.054), + (0.055, 1, 0.06), + (0.055, 2, 0.055), + # [0.1, 1) + (0.5, 1, 0.5), + (0.5, 2, 0.5), + (0.54, 1, 0.5), + (0.54, 2, 0.54), + (0.55, 1, 0.6), + (0.55, 2, 0.55), + # [1, 10) + (5, 1, 5), + (5, 2, 5), + (5.4, 1, 5), + (5.4, 2, 5.4), + (5.5, 1, 6), + (5.5, 2, 5.5), + # [10, 100) + (50, 1, 50), + (50, 2, 50), + (54, 1, 50), + (54, 2, 54), + (55, 1, 60), + (55, 2, 55), + # Overflow + (9.99, 1, 10), + (9.99, 2, 10), + # None + (None, 1, None), + ], + ids=[ + # Zero + "0", + "0.0", + # (0, 0.1) + "0.05 (1 sig fig)", + "0.05 (2 sig fig)", + "0.054 (1 sig fig)", + "0.054 (2 sig fig)", + "0.055 (1 sig fig)", + "0.055 (2 sig fig)", + # [0.1, 1) + "0.5 (1 sig fig)", + "0.5 (2 sig fig)", + "0.54 (1 sig fig)", + "0.54 (2 sig fig)", + "0.55 (1 sig fig)", + "0.55 (2 sig fig)", + # [1, 10) + "5 (1 sig fig)", + "5 (2 sig fig)", + "5.4 (1 sig fig)", + "5.4 (2 sig fig)", + "5.5 (1 sig fig)", + "5.5 (2 sig fig)", + # [10, 100) + "50 (1 sig fig)", + "50 (2 sig fig)", + "54 (1 sig fig)", + "54 (2 sig fig)", + "55 (1 sig fig)", + "55 (2 sig fig)", + # Overflow + "9.99 (1 sig fig)", + "9.99 (2 sig fig)", + # None + "None", + ], +) +def test_should_round_to_significant_figures( + value: float | None, + significant_figures: int, + expected: float | None, +) -> None: + assert_cell_operation_works( + value, + lambda cell: cell.math.round_to_significant_figures(significant_figures), + expected, + type_if_none=ColumnType.float64(), + ) + + +@pytest.mark.parametrize( + "significant_figures", + [ + -1, + 0, + ], + ids=[ + "negative", + "zero", + ], +) +def test_should_raise_if_significant_figures_is_out_of_bounds(significant_figures: int) -> None: + column = Column("a", [1]) + with pytest.raises(OutOfBoundsError): + column.transform(lambda cell: cell.math.round_to_significant_figures(significant_figures)) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_sign.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_sign.py new file mode 100644 index 000000000..b6b93df3a --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_sign.py @@ -0,0 +1,29 @@ +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (-2.5, -1), + (-1, -1), + (-0, -0), + (0, 0), + (1, 1), + (2.5, 1), + (None, None), + ], + ids=[ + "-2.5", + "-1", + "-0", + "0", + "1", + "2.5", + "None", + ], +) +def test_should_return_sign(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.sign(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_sin.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_sin.py new file mode 100644 index 000000000..078e850d3 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_sin.py @@ -0,0 +1,29 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 0), + (math.pi / 2, 1), + (math.pi, 0), + (3 * math.pi / 2, -1), + (2 * math.pi, 0), + (None, None), + ], + ids=[ + "0 deg", + "90 deg", + "180 deg", + "270 deg", + "360 deg", + "None", + ], +) +def test_should_return_sine(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.sin(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_sinh.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_sinh.py new file mode 100644 index 000000000..4540fd147 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_sinh.py @@ -0,0 +1,30 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + +E = math.e +PHI = (1 + math.sqrt(5)) / 2 + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 0), + (-1, -0.5 * (E - 1 / E)), + (1, 0.5 * (E - 1 / E)), + (math.log(PHI), 0.5), + (None, None), + ], + ids=[ + "0", + "-1", + "1", + "ln of golden ratio", + "None", + ], +) +def test_should_return_hyperbolic_sine(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.sinh(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_sqrt.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_sqrt.py new file mode 100644 index 000000000..72472d707 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_sqrt.py @@ -0,0 +1,28 @@ +import math + +import pytest + +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (-1, math.nan), + (0, 0), + (1, 1), + (2.25, 1.5), + (4, 2), + (None, None), + ], + ids=[ + "-1", + "0", + "1", + "square of 1.5", + "square of 2", + "None", + ], +) +def test_should_return_square_root(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.sqrt(), expected) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_tan.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_tan.py new file mode 100644 index 000000000..c70b4d773 --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_tan.py @@ -0,0 +1,29 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 0), + (math.pi / 4, 1), + (math.pi, 0), + (3 * math.pi / 4, -1), + (2 * math.pi, 0), + (None, None), + ], + ids=[ + "0 deg", + "45 deg", + "180 deg", + "135 deg", + "360 deg", + "None", + ], +) +def test_should_return_tangent(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.tan(), expected, type_if_none=ColumnType.float64()) diff --git a/tests/safeds/data/tabular/query/_lazy_math_operations/test_tanh.py b/tests/safeds/data/tabular/query/_lazy_math_operations/test_tanh.py new file mode 100644 index 000000000..255a792fa --- /dev/null +++ b/tests/safeds/data/tabular/query/_lazy_math_operations/test_tanh.py @@ -0,0 +1,30 @@ +import math + +import pytest + +from safeds.data.tabular.typing import ColumnType +from tests.helpers import assert_cell_operation_works + +E = math.e +PHI = (1 + math.sqrt(5)) / 2 + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (0, 0), + (-1, -(E - 1 / E) / (E + 1 / E)), + (1, (E - 1 / E) / (E + 1 / E)), + (math.log(PHI), 1 / math.sqrt(5)), + (None, None), + ], + ids=[ + "0", + "-1", + "1", + "ln of golden ratio", + "None", + ], +) +def test_should_return_hyperbolic_tangent(value: float | None, expected: float | None) -> None: + assert_cell_operation_works(value, lambda cell: cell.math.tanh(), expected, type_if_none=ColumnType.float64())