Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 8 additions & 8 deletions src/vtlengine/AST/ASTString.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,10 @@ def visit_HRuleset(self, node: AST.HRuleset) -> None:
self.vtl_script += f"define hierarchical ruleset {node.name}({signature}) is{nl}"
for _i, rule in enumerate(node.rules):
rule_str = f"{tab}{self.visit(rule)}"
if rule.erCode:
if rule.erCode is not None:
rule_str += f"{nl}{tab}errorcode {_handle_literal(rule.erCode)}"
if rule.erLevel:
rule_str += f"{nl}{tab}errorlevel {rule.erLevel}"
if rule.erLevel is not None:
rule_str += f"{nl}{tab}errorlevel {_handle_literal(rule.erLevel)}"
rules_strs.append(rule_str)
rules_sep = f";{nl * 2}" if len(rules_strs) > 1 else ""
rules = rules_sep.join(rules_strs)
Expand All @@ -146,10 +146,10 @@ def visit_HRuleset(self, node: AST.HRuleset) -> None:
else:
for rule in node.rules:
rule_str = self.visit(rule)
if rule.erCode:
if rule.erCode is not None:
rule_str += f" errorcode {_handle_literal(rule.erCode)}"
if rule.erLevel:
rule_str += f" errorlevel {rule.erLevel}"
if rule.erLevel is not None:
rule_str += f" errorlevel {_handle_literal(rule.erLevel)}"
rules_strs.append(rule_str)
rules_sep = "; " if len(rules_strs) > 1 else ""
rules = rules_sep.join(rules_strs)
Expand Down Expand Up @@ -195,7 +195,7 @@ def visit_DPRule(self, node: AST.DPRule) -> str:
if node.erCode is not None:
lines.append(f"{tab * 3}errorcode {_handle_literal(node.erCode)}")
if node.erLevel is not None:
lines.append(f"{tab * 3}errorlevel {node.erLevel}")
lines.append(f"{tab * 3}errorlevel {_handle_literal(node.erLevel)}")
return nl.join(lines)
else:
vtl_script = ""
Expand All @@ -205,7 +205,7 @@ def visit_DPRule(self, node: AST.DPRule) -> str:
if node.erCode is not None:
vtl_script += f" errorcode {_handle_literal(node.erCode)}"
if node.erLevel is not None:
vtl_script += f" errorlevel {node.erLevel}"
vtl_script += f" errorlevel {_handle_literal(node.erLevel)}"
return vtl_script

def visit_DPRIdentifier(self, node: AST.DPRIdentifier) -> str:
Expand Down
12 changes: 6 additions & 6 deletions src/vtlengine/AST/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,8 +484,8 @@ class Validation(AST):

op: str
validation: AST
error_code: Optional[str]
error_level: Optional[Union[int, str]]
error_code: Optional[Union[str, int, float, bool]]
error_level: Optional[Union[str, int, float, bool]]
imbalance: Optional[AST]
invalid: bool

Expand Down Expand Up @@ -631,8 +631,8 @@ class HRule(AST):

name: Optional[str]
rule: HRBinOp
erCode: Optional[str]
erLevel: Optional[Union[int, str]]
erCode: Optional[Union[str, int, float, bool]]
erLevel: Optional[Union[str, int, float, bool]]

__eq__ = AST.ast_equality

Expand All @@ -645,8 +645,8 @@ class DPRule(AST):

name: Optional[str]
rule: HRBinOp
erCode: Optional[str]
erLevel: Optional[Union[int, str]]
erCode: Optional[Union[str, int, float, bool]]
erLevel: Optional[Union[str, int, float, bool]]

__eq__ = AST.ast_equality

Expand Down
36 changes: 21 additions & 15 deletions src/vtlengine/Operators/Validation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from copy import copy
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, Optional, Type, Union

import pandas as pd

Expand All @@ -8,6 +8,7 @@
Boolean,
Integer,
Number,
ScalarType,
String,
check_unary_implicit_promotion,
)
Expand All @@ -26,8 +27,8 @@ def validate(
cls,
validation_element: Dataset,
imbalance_element: Optional[Dataset],
error_code: Optional[str],
error_level: Optional[Union[int, str]],
error_code: Optional[Union[str, int, float, bool]],
error_level: Optional[Union[str, int, float, bool]],
invalid: bool,
) -> Dataset:
dataset_name = VirtualCounter._new_ds_name()
Expand All @@ -36,11 +37,13 @@ def validate(
measure = validation_element.get_measures()[0]
if measure.data_type != Boolean:
raise SemanticError("1-1-10-1", op=cls.op, op_type="validation", me_type="Boolean")
error_level_type = None
if error_level is None or isinstance(error_level, int):
error_level_type: Optional[Type[ScalarType]] = None
if isinstance(error_level, bool):
error_level_type = Boolean
elif error_level is None or isinstance(error_level, int):
error_level_type = Integer
elif isinstance(error_level, str):
error_level_type = String # type: ignore[assignment]
error_level_type = String
else:
error_level_type = String

Expand Down Expand Up @@ -79,7 +82,7 @@ def validate(

result_components["errorlevel"] = Component(
name="errorlevel",
data_type=error_level_type, # type: ignore[arg-type]
data_type=error_level_type,
role=Role.MEASURE,
nullable=True,
)
Expand All @@ -91,8 +94,8 @@ def evaluate(
cls,
validation_element: Dataset,
imbalance_element: Optional[Dataset],
error_code: Optional[str],
error_level: Optional[Union[int, str]],
error_code: Optional[Union[str, int, float, bool]],
error_level: Optional[Union[str, int, float, bool]],
invalid: bool,
) -> Dataset:
result = cls.validate(
Expand All @@ -116,7 +119,8 @@ def evaluate(
bool_col = result.data[validation_measure_name]
is_false = bool_col.fillna(True) == False # noqa: E712
result.data["errorcode"] = pd.Series(None, index=result.data.index, dtype="string[pyarrow]")
result.data.loc[is_false, "errorcode"] = error_code
ec_value = str(error_code) if error_code is not None else None
result.data.loc[is_false, "errorcode"] = ec_value
errorlevel_dtype = result.components["errorlevel"].data_type.dtype()
result.data["errorlevel"] = pd.Series(None, index=result.data.index, dtype=errorlevel_dtype)
if error_level is not None:
Expand Down Expand Up @@ -150,20 +154,22 @@ def _generate_result_data(cls, rule_info: Dict[str, Any]) -> pd.DataFrame:

@classmethod
def validate(cls, dataset_element: Dataset, rule_info: Dict[str, Any], output: str) -> Dataset:
error_level_type = None
error_level_type: Optional[Type[ScalarType]] = None
error_levels = [
rule_data.get("errorlevel")
for rule_data in rule_info.values()
if "errorlevel" in rule_data
]
non_null_levels = [el for el in error_levels if el is not None]

if len(non_null_levels) == 0 or all(isinstance(el, int) for el in non_null_levels):
if all(isinstance(el, bool) for el in non_null_levels) and len(non_null_levels) > 0:
error_level_type = Boolean
elif len(non_null_levels) == 0 or all(isinstance(el, int) for el in non_null_levels):
error_level_type = Number
elif all(isinstance(el, str) for el in non_null_levels):
error_level_type = String # type: ignore[assignment]
error_level_type = String
else:
error_level_type = String # type: ignore[assignment]
error_level_type = String
dataset_name = VirtualCounter._new_ds_name()
result_components = {comp.name: comp for comp in dataset_element.get_identifiers()}
result_components["ruleid"] = Component(
Expand Down Expand Up @@ -191,7 +197,7 @@ def validate(cls, dataset_element: Dataset, rule_info: Dict[str, Any], output: s
)
result_components["errorlevel"] = Component(
name="errorlevel",
data_type=error_level_type, # type: ignore[arg-type]
data_type=error_level_type,
role=Role.MEASURE,
nullable=True,
)
Expand Down
5 changes: 5 additions & 0 deletions tests/DatapointRulesets/data/DataSet/input/GH_598_2-1.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Id_1,Id_2,Me_1
A,1,10
A,2,-5
B,1,100
B,2,30
3 changes: 3 additions & 0 deletions tests/DatapointRulesets/data/DataSet/output/GH_598_2-1.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Id_1,Id_2,ruleid,Me_1,errorcode,errorlevel
A,2,rule_1,-5.0,True,True
B,2,rule_2,30.0,False,False
27 changes: 27 additions & 0 deletions tests/DatapointRulesets/data/DataStructure/input/GH_598_2-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"datasets": [
{
"name": "DS_1",
"DataStructure": [
{
"name": "Id_1",
"role": "Identifier",
"type": "String",
"nullable": false
},
{
"name": "Id_2",
"role": "Identifier",
"type": "Integer",
"nullable": false
},
{
"name": "Me_1",
"role": "Measure",
"type": "Number",
"nullable": true
}
]
}
]
}
45 changes: 45 additions & 0 deletions tests/DatapointRulesets/data/DataStructure/output/GH_598_2-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"datasets": [
{
"name": "DS_r",
"DataStructure": [
{
"name": "Id_1",
"role": "Identifier",
"type": "String",
"nullable": false
},
{
"name": "Id_2",
"role": "Identifier",
"type": "Integer",
"nullable": false
},
{
"name": "ruleid",
"role": "Identifier",
"type": "String",
"nullable": false
},
{
"name": "Me_1",
"role": "Measure",
"type": "Number",
"nullable": true
},
{
"name": "errorcode",
"role": "Measure",
"type": "String",
"nullable": true
},
{
"name": "errorlevel",
"role": "Measure",
"type": "Boolean",
"nullable": true
}
]
}
]
}
6 changes: 6 additions & 0 deletions tests/DatapointRulesets/data/vtl/GH_598_2.vtl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
define datapoint ruleset dp_bool (variable Id_1 as X, Me_1 as M) is
rule_1: when X = "A" then M > 0 errorcode true errorlevel true;
rule_2: when X = "B" then M > 50 errorcode false errorlevel false
end datapoint ruleset;

DS_r <- check_datapoint(DS_1, dp_bool);
43 changes: 43 additions & 0 deletions tests/DatapointRulesets/test_datapoint_rulesets.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from pathlib import Path

from tests.Helper import TestHelper
from vtlengine.API import create_ast
from vtlengine.AST.ASTString import ASTString


class TestDataPointRuleset(TestHelper):
Expand Down Expand Up @@ -420,3 +422,44 @@ def test_14(self):
self.NewSemanticExceptionTest(
code=code, number_inputs=number_inputs, exception_code=message
)

def test_GH_598_2(self):
"""
check_datapoint with boolean errorcode and errorlevel constants.
Dataset --> Dataset
Status: OK

define datapoint ruleset dp_bool (variable Id_1 as X, Me_1 as M) is
rule_1: when X = "A" then M > 0 errorcode true errorlevel true;
rule_2: when X = "B" then M > 50 errorcode false errorlevel false
end datapoint ruleset;

DS_r <- check_datapoint(DS_1, dp_bool);

Git Branch: cr-596.
Goal: Verify boolean constants work as errorcode/errorlevel in datapoint
rulesets, including AST round-trip.
"""
code = "GH_598_2"
number_inputs = 1
references_names = ["1"]

self.BaseTest(code=code, number_inputs=number_inputs, references_names=references_names)

def test_GH_598_2_roundtrip(self):
"""
AST round-trip for check_datapoint with boolean errorcode/errorlevel.

Git Branch: cr-596.
Goal: Verify that rendering the AST back to VTL and re-parsing produces the
same result when boolean constants are used in errorcode/errorlevel.
"""
code = "GH_598_2"
number_inputs = 1
references_names = ["1"]

text = self.LoadVTL(code)
rendered = ASTString().render(create_ast(text))
self.BaseTest(
code=code, number_inputs=number_inputs, references_names=references_names, text=rendered
)
7 changes: 7 additions & 0 deletions tests/Hierarchical/data/DataSet/input/GH_598_1-1.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Id_1,Id_2,Me_1
B,XX,100
C,XX,80
D,XX,30
N,XX,200
A,XX,190
L,XX,10
3 changes: 3 additions & 0 deletions tests/Hierarchical/data/DataSet/output/GH_598_1-1.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Id_1,Id_2,ruleid,Me_1,errorcode,errorlevel,imbalance
B,XX,1,100.0,True,True,50.0
N,XX,2,200.0,False,False,20.0
27 changes: 27 additions & 0 deletions tests/Hierarchical/data/DataStructure/input/GH_598_1-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"datasets": [
{
"name": "DS_1",
"DataStructure": [
{
"name": "Id_1",
"role": "Identifier",
"type": "String",
"nullable": false
},
{
"name": "Id_2",
"role": "Identifier",
"type": "String",
"nullable": false
},
{
"name": "Me_1",
"role": "Measure",
"type": "Number",
"nullable": true
}
]
}
]
}
Loading
Loading