Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
87 changes: 81 additions & 6 deletions src/cr/cube/dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
MARGINAL,
MEASURE,
)
from cr.cube.util import lazyproperty
from cr.cube.util import common_prefix, common_suffix, lazyproperty

from .util import format, format_datetime

Expand All @@ -33,7 +33,67 @@
}


def _formatter(dimension_type, typedef, out_format) -> Union[Callable, partial]:
class LabelTransformFuncs:
def __init__(self, label_transforms: list[dict], all_labels: list[str]):
"""
Used to apply the label function transformations on elements of a
dimension

:param label_transforms: list of dictionary functions. See
Dashboard-models for the list of options and arguments
:param all_labels: list of all the original labels of the dimension. It
is necessary to have them all so we can do the prefix/suffix trimming.
"""
self.label_transforms = label_transforms
self.all_labels = all_labels

def apply(self, formatter: Union[Callable, partial]) -> Callable[[str], str]:
"""
Receives the formatter and will wrap any other transformation
on top of its output.
:param formatter: Callable that receives a sting and outputs a string
:return: A callable with the same signature as the formatter
"""
if not self.label_transforms:
return formatter

def __inner(value: str) -> str:
for func_transform in self.label_transforms:
# Invalid function raises AttributeError, this should break
# because it's an incomplete implementation. The functions
# should be specified in the Lark syntax and Dashboard models
# to match the list of allowed operations here. Requires
# 3 repos to be updated.
func = getattr(self, func_transform["function"])
args = func_transform["args"]
value = func(value, args)

value = formatter(value)
return value

return __inner

@staticmethod
def replace(value: str, args) -> str:
return value.replace(*args)

def remove_common_suffix(self, value: str, args) -> str:
suffix_pos = common_suffix(self.all_labels)
return value[:suffix_pos] if suffix_pos else value

def remove_common_prefix(self, value: str, args) -> str:
prefix_pos = common_prefix(self.all_labels)
return value[prefix_pos:] if prefix_pos else prefix_pos

def trim_common(self, value: str, args) -> str:
no_suffix = self.remove_common_suffix(value, [])
no_prefix_either = self.remove_common_prefix(no_suffix, [])
return no_prefix_either


def _formatter(
dimension_type, typedef, out_format, label_transforms, all_labels
) -> Union[Callable, partial]:
"""Returns a formatting function according to the dimension type."""

if dimension_type != DT.DATETIME:
Expand All @@ -46,6 +106,10 @@ def _formatter(dimension_type, typedef, out_format) -> Union[Callable, partial]:
if orig_format and out_format
else format
)

if label_transforms: # Apply label transforms on the output
formatter = LabelTransformFuncs(label_transforms, all_labels).apply(formatter)

return formatter


Expand Down Expand Up @@ -534,6 +598,7 @@ def from_typedef(
element_defs = [codemap[code] for code in order if code in codemap]

all_xforms = dimension_transforms_dict.get("elements", {})
label_transforms = dimension_transforms_dict.get("label_transforms")
if dimension_type == DT.MR_SUBVAR:
hidden_xforms = cls._hidden_transforms(
element_defs,
Expand All @@ -542,13 +607,20 @@ def from_typedef(
all_xforms = {**hidden_xforms, **all_xforms}

elements = []
all_labels = [elt["name"] for elt in element_defs if "name" in elt]
for idx, element_dict in enumerate(element_defs):
# --- convert to string for categorical ids
element_id = _build_element_id(element_dict, dimension_type)
xforms = _ElementTransforms(
all_xforms.get(element_id, all_xforms.get(str(element_id), {}))
)
formatter = _formatter(dimension_type, typedef, element_data_format)
formatter = _formatter(
dimension_type,
typedef,
element_data_format,
label_transforms,
all_labels,
)
element = Element(element_dict, idx, xforms, formatter, dimension_type)
elements.append(element)

Expand Down Expand Up @@ -1037,15 +1109,17 @@ def _str_representation_for(self, key: str) -> str:
# ---first authority is transform in element transforms---
value = getattr(self._element_transforms, key) if key == "name" else None
if value is not None:
return value if value else ""
value = value if value else ""
return self._label_formatter(value)

# ---otherwise base-name/alias from element-dict is used according to the key---
element_dict = self._element_dict

# ---category elements have a name/alias item according to the key---
if key in element_dict:
value = element_dict[key]
return value if value else ""
value = value if value else ""
return self._label_formatter(value)

# ---other types are more complicated---
value = element_dict.get("value")
Expand All @@ -1062,7 +1136,8 @@ def _str_representation_for(self, key: str) -> str:
return self._label_formatter(value)

# ---For CA and MR subvar dimensions---
return value.get("references", {}).get(key) or ""
value = value.get("references", {}).get(key) or ""
return self._label_formatter(value)


class _ElementTransforms:
Expand Down
19 changes: 19 additions & 0 deletions src/cr/cube/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,22 @@ def __set__(self, obj, value):
quite snappy and probably not a rich target for optimization efforts.
"""
raise AttributeError("can't set attribute")


def common_prefix(str_list):
prefix_pos = 0
for pos, char in enumerate(str_list[0]):
try:
if {s[pos] for s in str_list[1:]} != {char}:
break
prefix_pos += 1
except IndexError:
# This means the pivot is now beyond the shortest word, this
# means that word is the common prefix
return prefix_pos
return prefix_pos


def common_suffix(str_list):
str_list = [list(reversed(s)) for s in str_list]
return -1 * common_prefix(str_list) # ;)
65 changes: 56 additions & 9 deletions tests/unit/test_dimension.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
import pytest

from cr.cube.dimension import (
Element,
Elements,
Dimension,
Dimensions,
Element,
Elements,
LabelTransformFuncs,
_ElementIdShim,
_ElementTransforms,
_OrderSpec,
Expand All @@ -23,13 +24,7 @@
MEASURE,
)

from ..unitutil import (
call,
class_mock,
instance_mock,
method_mock,
property_mock,
)
from ..unitutil import call, class_mock, instance_mock, method_mock, property_mock


class TestDimensions:
Expand Down Expand Up @@ -1190,6 +1185,25 @@ def test_it_knows_its_label(
element = Element(element_dict, None, element_transforms_, str, None)
assert element.label == expected_value

def test_label_function(self):
typedef = {
"class": "categorical",
"categories": [
{"id": 1, "name": "xx 1. REPLaced"},
{"id": 2, "name": "xx 2. REPLace me"},
],
}
transforms_dict = {
"label_transforms": [
{"function": "replace", "args": ["REPL", "repl"]},
{"function": "trim_common", "args": []},
{"function": "replace", "args": ["me", "you"]},
],
}
elements = Elements.from_typedef(typedef, transforms_dict, DT.CAT, None)
assert elements[0].label == "1. replaced"
assert elements[1].label == "2. replace you"

@pytest.mark.parametrize(
("hide", "expected_value"), ((True, True), (False, False), (None, False))
)
Expand Down Expand Up @@ -1773,3 +1787,36 @@ def subtrahend_ids_(self, request):
@pytest.fixture
def valid_elements_(self, request):
return instance_mock(request, Elements)


class TestLabelTransformFuncs:
def test_prefix(self):
all_labels = ["a1", "a2", "a3"]
transformer = LabelTransformFuncs([], all_labels)
assert transformer.remove_common_prefix("a2", []) == "2"

def test_suffix(self):
all_labels = ["a1b", "a2b", "a3b"]
transformer = LabelTransformFuncs([], all_labels)
assert transformer.remove_common_suffix("a2b", []) == "a2"

def test_trim_common(self):
all_labels = ["a1b", "a2b", "a3b"]
transformer = LabelTransformFuncs([], all_labels)
assert transformer.trim_common("a2b", []) == "2"

def test_replace(self):
all_labels = ["a1b", "a2b", "a3b"]
transformer = LabelTransformFuncs([], all_labels)
assert transformer.replace("a2b", ["a", "AA"]) == "AA2b"

def test_application(self):
all_labels = ["a1b", "a2b", "a3b"]
transforms = [
{"function": "trim_common", "args": []},
{"function": "replace", "args": ["1", "one"]},
{"function": "replace", "args": ["2", "two"]},
]
transformer = LabelTransformFuncs(transforms, all_labels)
formatter = transformer.apply(lambda x: x.upper())
assert formatter("a2b") == "TWO"
Loading