diff --git a/src/dask_awkward/__init__.py b/src/dask_awkward/__init__.py index 50e3f879..7d09641a 100644 --- a/src/dask_awkward/__init__.py +++ b/src/dask_awkward/__init__.py @@ -6,6 +6,7 @@ import dask_awkward.lib.operations as operations import dask_awkward.lib.optimize as optimize import dask_awkward.lib.reducers as reducers +import dask_awkward.lib.str as str import dask_awkward.lib.structure as structure from dask_awkward.lib.core import Array, PartitionCompatibility, Record, Scalar from dask_awkward.lib.core import _type as type diff --git a/src/dask_awkward/lib/__init__.py b/src/dask_awkward/lib/__init__.py index 5be5dcce..7a51116b 100644 --- a/src/dask_awkward/lib/__init__.py +++ b/src/dask_awkward/lib/__init__.py @@ -1,3 +1,4 @@ +import dask_awkward.lib.str as str from dask_awkward.lib.core import Array, PartitionCompatibility, Record, Scalar from dask_awkward.lib.core import _type as type from dask_awkward.lib.core import ( diff --git a/src/dask_awkward/lib/core.py b/src/dask_awkward/lib/core.py index c0b6861f..b9950dac 100644 --- a/src/dask_awkward/lib/core.py +++ b/src/dask_awkward/lib/core.py @@ -1273,7 +1273,12 @@ def __awkward_function__(self, func, array_likes, args, kwargs): try: fn = getattr(dask_awkward, fn_name) except AttributeError: - return NotImplemented + try: + import dask_awkward.lib.str + + fn = getattr(dask_awkward.str, fn_name) + except AttributeError: + return NotImplemented return fn(*args, **kwargs) def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): diff --git a/src/dask_awkward/lib/str.py b/src/dask_awkward/lib/str.py new file mode 100644 index 00000000..4be391f9 --- /dev/null +++ b/src/dask_awkward/lib/str.py @@ -0,0 +1,873 @@ +from __future__ import annotations + +import functools + +import awkward.operations.str as akstr + +from dask_awkward.lib.core import Array, map_partitions + + +def always_highlevel(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + if not kwargs.get("highlevel", True): + raise ValueError("dask-awkward supports only highlevel awkward arrays.") + return fn(*args, **kwargs) + + return wrapper + + +@always_highlevel +def capitalize( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.capitalize, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def center( + array: Array, + width, + padding=" ", + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.center, + array, + width=width, + padding=padding, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def count_substring( + array: Array, + pattern: str | bytes, + *, + ignore_case: bool = False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.count_substring, + array, + pattern=pattern, + ignore_case=ignore_case, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def count_substring_regex( + array: Array, + pattern: str | bytes, + *, + ignore_case: bool = False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.count_substring_regex, + array, + pattern=pattern, + ignore_case=ignore_case, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def ends_with( + array: Array, + pattern: str | bytes, + *, + ignore_case: bool = False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.ends_with, + array, + pattern=pattern, + ignore_case=ignore_case, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def extract_regex( + array: Array, + pattern: bytes | str, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.extract_regex, + array, + pattern=pattern, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def find_substring( + array: Array, + pattern, + *, + ignore_case=False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.find_substring, + array, + pattern=pattern, + ignore_case=ignore_case, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def find_substring_regex( + array: Array, + pattern, + *, + ignore_case=False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.find_substring_regex, + array, + pattern=pattern, + ignore_case=ignore_case, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def index_in( + array: Array, + value_set, + *, + skip_nones=False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.index_in, + array, + value_set=value_set, + skip_nones=skip_nones, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_alnum( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_alnum, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_alpha( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_alpha, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_ascii( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_ascii, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_decimal( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_decimal, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_digit( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_digit, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_in( + array: Array, + value_set, + *, + skip_nones=False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_in, + array, + value_set=value_set, + skip_nones=skip_nones, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_lower( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_lower, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_numeric( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_numeric, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_printable( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_printable, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_space( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_space, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_title( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_title, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def is_upper( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.is_upper, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def join( + array: Array, + separator, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.join, + array, + separator=separator, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def join_element_wise( + *arrays, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.join_element_wise, + *arrays, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def length( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.length, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def lower( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.lower, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def lpad( + array: Array, + width, + padding=" ", + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.lpad, + array, + width=width, + padding=padding, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def ltrim( + array: Array, + characters, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.ltrim, + array, + characters=characters, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def ltrim_whitespace( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.ltrim_whitespace, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def match_like( + array: Array, + pattern, + *, + ignore_case=False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.match_like, + array, + pattern=pattern, + ignore_case=ignore_case, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def match_substring( + array: Array, + pattern, + *, + ignore_case=False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.match_substring, + array, + pattern=pattern, + ignore_case=ignore_case, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def match_substring_regex( + array: Array, + pattern, + *, + ignore_case=False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.match_substring_regex, + array, + pattern=pattern, + ignore_case=ignore_case, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def repeat( + array: Array, + num_repeats, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.repeat, + array, + num_repeats=num_repeats, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def replace_slice( + array: Array, + start, + stop, + replacement, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.replace_slice, + array, + start=start, + stop=stop, + replacement=replacement, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def replace_substring( + array: Array, + pattern, + replacement, + *, + max_replacements=None, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.replace_substring, + array, + pattern=pattern, + replacement=replacement, + max_replacements=max_replacements, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def replace_substring_regex( + array: Array, + pattern, + replacement, + *, + max_replacements=None, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.replace_substring_regex, + array, + pattern=pattern, + replacement=replacement, + max_replacements=max_replacements, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def reverse( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.reverse, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def rpad( + array: Array, + width, + padding=" ", + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.rpad, + array, + width=width, + padding=padding, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def rtrim( + array: Array, + characters, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.rtrim, + array, + characters=characters, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def rtrim_whitespace( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.rtrim_whitespace, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def slice( + array: Array, + start, + stop=None, + step=1, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.slice, + array, + start=start, + stop=stop, + step=step, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def split_pattern( + array: Array, + pattern, + *, + max_splits=None, + reverse=False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.split_pattern, + array, + pattern=pattern, + max_splits=max_splits, + reverse=reverse, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def split_pattern_regex( + array: Array, + pattern, + *, + max_splits=None, + reverse=False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.split_pattern_regex, + array, + pattern=pattern, + max_splits=max_splits, + reverse=reverse, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def split_whitespace( + array: Array, + *, + max_splits: int | None = None, + reverse: bool = False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.split_whitespace, + array, + max_splits=max_splits, + reverse=reverse, + behavior=behavior, + ) + + +@always_highlevel +def starts_with( + array: Array, + pattern, + *, + ignore_case=False, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.starts_with, + array, + pattern=pattern, + ignore_case=ignore_case, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def swapcase( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.swapcase, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def title( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.title, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def to_categorical( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.to_categorical, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def trim( + array: Array, + characters, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.trim, + array, + characters=characters, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def trim_whitespace( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.trim_whitespace, + array, + behavior=behavior, + output_divisions=1, + ) + + +@always_highlevel +def upper( + array: Array, + *, + highlevel: bool = True, + behavior: dict | None = None, +) -> Array: + return map_partitions( + akstr.upper, + array, + behavior=behavior, + output_divisions=1, + ) diff --git a/tests/test_str.py b/tests/test_str.py new file mode 100644 index 00000000..2cbad17d --- /dev/null +++ b/tests/test_str.py @@ -0,0 +1,328 @@ +from __future__ import annotations + +import pytest + +pytest.importorskip("pyarrow") + +import awkward as ak +import awkward.operations.str as akstr + +import dask_awkward as dak +from dask_awkward.lib.testutils import assert_eq + +lines1 = [ + "this is line one", + "123", + "5", + " ", + " 12.34" " 123 ", + "this is line two", + "Ok Cool", + "THIS IS LINE THREE", + "OKOKOK", + " aaaaaaa ", + "42.52", + "OKOK", +] + +lines2 = [ + "1", + "aaaaaaaaaaa", + "bbbbbbbbbbbbbbbb", + "CCC", + " ", + "OK", + "DDDDDDDDDDDDDDDDDDDDDD", + " aaa ", + "Aa Bb Cc", +] + +lines3 = [ + " a ", + " aaa ", + " aaaaa ", + " aaaaaaa ", + " aaaaaaaaa ", + " aaaaaaa ", + " aaaaa ", + " aaa ", + " a ", +] + +caa = ak.from_iter(lines1 + lines2 + lines3) +daa = dak.from_lists([lines1, lines2, lines3]) + + +def test_sanity(): + assert_eq(caa, daa) + + +def test_capitalize() -> None: + assert_eq( + dak.str.capitalize(daa), + akstr.capitalize(caa), + ) + + +def test_center() -> None: + assert_eq( + dak.str.center(daa, 5), + akstr.center(caa, 5), + ) + + +def test_count_substring() -> None: + assert_eq( + dak.str.count_substring(daa, "aa"), + akstr.count_substring(caa, "aa"), + ) + + +def test_count_substring_regex() -> None: + a = akstr.count_substring_regex(daa, r"aa\s+") + assert isinstance(a, dak.Array) + b = akstr.count_substring_regex(caa, r"aa\s+") + assert_eq(a, b) + + +def test_ends_with() -> None: + assert_eq(akstr.ends_with(daa, "123"), akstr.ends_with(caa, "123")) + + +def test_extract_regex() -> None: + pass + + +def test_find_substring() -> None: + assert_eq(akstr.find_substring(daa, r"bbb"), akstr.find_substring(caa, r"bbb")) + + +def test_find_substring_regex() -> None: + assert_eq( + akstr.find_substring_regex(daa, r"aa\s+"), + akstr.find_substring_regex(caa, r"aa\s+"), + ) + + +def test_index_in() -> None: + assert_eq( + akstr.index_in(daa, [" aaa ", "123"]), + akstr.index_in(caa, [" aaa ", "123"]), + ) + + +def test_is_alnum() -> None: + assert_eq(akstr.is_alnum(daa), akstr.is_alnum(caa)) + + +def test_is_alpha() -> None: + assert_eq(akstr.is_alpha(daa), akstr.is_alpha(caa)) + + +def test_is_ascii() -> None: + assert_eq(akstr.is_ascii(daa), akstr.is_ascii(caa)) + + +def test_is_decimal() -> None: + assert_eq(akstr.is_decimal(daa), akstr.is_decimal(caa)) + + +def test_is_digit() -> None: + assert_eq(akstr.is_digit(daa), akstr.is_digit(caa)) + + +def test_is_in() -> None: + assert_eq( + akstr.is_in(daa, ["CCC", "1"]), + akstr.is_in(caa, ["CCC", "1"]), + ) + + +def test_is_lower() -> None: + assert_eq(akstr.is_lower(daa), akstr.is_lower(caa)) + + +def test_is_numeric() -> None: + assert_eq(akstr.is_numeric(daa), akstr.is_numeric(caa)) + + +def test_is_printable() -> None: + assert_eq(akstr.is_printable(daa), akstr.is_printable(caa)) + + +def test_is_space() -> None: + assert_eq(akstr.is_space(daa), akstr.is_space(caa)) + + +def test_is_title() -> None: + assert_eq(akstr.is_title(daa), akstr.is_title(caa)) + + +def test_is_upper() -> None: + assert_eq(akstr.is_upper(daa), akstr.is_upper(caa)) + + +def test_join() -> None: + a = [["one two three", "four five"], ["six seven"]] + caa = ak.Array(a) + daa = dak.from_awkward(caa, npartitions=2) + assert_eq( + akstr.join(akstr.split_whitespace(daa), ","), + akstr.join(akstr.split_whitespace(caa), ","), + ) + + +def test_join_element_wise() -> None: + pass + + +def test_length() -> None: + assert_eq(akstr.length(daa), akstr.length(caa)) + + +def test_lower() -> None: + assert_eq(akstr.lower(daa), akstr.lower(caa)) + + +def test_lpad() -> None: + assert_eq(akstr.lpad(daa, 3), akstr.lpad(caa, 3)) + + +def test_ltrim() -> None: + assert_eq( + akstr.ltrim(akstr.ltrim(daa, "th"), " "), + akstr.ltrim(akstr.ltrim(caa, "th"), " "), + ) + + +def test_ltrim_whitespace() -> None: + assert_eq(akstr.ltrim_whitespace(daa), akstr.ltrim_whitespace(caa)) + + +def test_match_like() -> None: + assert_eq(akstr.match_like(daa, "this%"), akstr.match_like(caa, "this%")) + + +def test_match_substring() -> None: + assert_eq(akstr.match_like(daa, " aaa"), akstr.match_like(caa, " aaa")) + + +def test_match_substring_regex() -> None: + assert_eq( + akstr.match_substring_regex(daa, r"\w+aaa\w+"), + akstr.match_substring_regex(caa, r"\w+aaa\w+"), + ) + + +def test_repeat() -> None: + assert_eq(akstr.repeat(daa, 3), akstr.repeat(caa, 3)) + + +def test_replace_slice() -> None: + a = akstr.replace_slice(daa, start=2, stop=12, replacement="...") + b = akstr.replace_slice(caa, start=2, stop=12, replacement="...") + assert_eq(a, b) + + +def test_replace_substring() -> None: + assert_eq( + akstr.replace_substring(daa, "aaa", "ZZZ"), + akstr.replace_substring(caa, "aaa", "ZZZ"), + ) + + +def test_replace_substring_regex() -> None: + pass + + +def test_reverse() -> None: + assert_eq(akstr.reverse(daa), akstr.reverse(caa)) + + +def test_rpad() -> None: + assert_eq(akstr.rpad(daa, 5, "j"), akstr.rpad(caa, 5, "j")) + + +def test_rtrim() -> None: + assert_eq( + akstr.rtrim(akstr.rtrim(daa, "OK"), " "), + akstr.rtrim(akstr.rtrim(caa, "OK"), " "), + ) + + +def test_rtrim_whitespace() -> None: + assert_eq(akstr.rtrim_whitespace(daa), akstr.rtrim_whitespace(caa)) + + +@pytest.mark.parametrize( + "args", + [ + (2, 10, 2), + (3, None, 1), + (0, None, 3), + ], +) +def test_slice(args) -> None: + start, stop, step = args + assert_eq(akstr.slice(daa, start, stop, step), akstr.slice(caa, start, stop, step)) + + +def test_split_pattern() -> None: + assert_eq( + akstr.split_pattern(daa, "aa"), + akstr.split_pattern(caa, "aa"), + ) + + +def test_split_pattern_regex() -> None: + assert_eq( + akstr.split_pattern_regex(daa, r"[0-9]{2}"), + akstr.split_pattern_regex(caa, r"[0-9]{2}"), + ) + + +def test_split_whitespace() -> None: + a = [["one two three", "four five"], ["six seven"]] + caa = ak.Array(a) + daa = dak.from_awkward(caa, npartitions=2) + assert_eq( + akstr.split_whitespace(daa), + akstr.split_whitespace(caa), + ) + + +def test_starts_with() -> None: + assert_eq( + akstr.starts_with(daa, " "), + akstr.starts_with(caa, " "), + ) + + +def test_swapcase() -> None: + assert_eq(akstr.swapcase(daa), akstr.swapcase(caa)) + + +def test_title() -> None: + assert_eq(akstr.title(daa), akstr.title(caa)) + + +def test_to_categorical() -> None: + a = akstr.to_categorical(daa) + b = akstr.to_categorical(caa) + assert set(a.compute().layout.content.to_list()) == set(b.layout.content.to_list()) + assert_eq(a, b) + + +def test_trim() -> None: + assert_eq( + akstr.trim(daa, "123"), + akstr.trim(caa, "123"), + ) + + +def test_trim_whitespace() -> None: + assert_eq(akstr.trim_whitespace(daa), akstr.trim_whitespace(caa)) + + +def test_upper() -> None: + assert_eq(akstr.upper(daa), akstr.upper(caa))