diff --git a/README.md b/README.md index e5abe43..4798375 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ decision_table = { "rows": [ # 行对应的值 ["[0..70]", '"boy"'], ["(70..80]", '"boy"'], - ["(80..90]", '"boy"'], # 对于实际场景中,如果希望一行表示多列表达式,则直接用字符串类型进行描述,形如 'score in (80..90] and sex="boy"' + ["(80..90]", '"boy"'], # 对于实际场景中,如果希望【单行表示多列表达式】,则直接用字符串类型进行描述,形如 'score in (80..90] and sex="boy"' ["(90..100]", '"boy"'], ["[0..60]", '"girl"'], ["(60..70]", '"girl"'], @@ -47,6 +47,26 @@ decision_table = { } ``` +其中,输入表默认会将单元格中的表达式作为整体表达式进行真值判定,同时支持一些快捷填写方式(仅在未使用【单行表示多列表达式】的情况下): + +| 快捷填写方式 | 补全后表达式(假设列名为 x) | +| --- | --- | +| ">=10" | "x>=10" | +| ">10" | "x>10" | +| "<=10" | "x<=10" | +| "<10" | "x<10" | +| "[1..2]" | "x in [1..2]" | +| "(1..2]" | "x in (1..2]" | +| '"boy"' | 'x="boy"' | +| "1" | "x=1" | +| "1.2" | "x=1.2" | +| "-1" | "x=-1" | +| "-2.3" | "x=-2.3" | +| "true" | "x=true" | +| "false" | "x=false" | +| "null" | "x=null" | + + ### 3. 调用函数进行决策 ``` python diff --git a/bkflow_dmn/__version__.py b/bkflow_dmn/__version__.py index b37a8d5..3e129c8 100644 --- a/bkflow_dmn/__version__.py +++ b/bkflow_dmn/__version__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = "0.1.1" +__version__ = "0.2.0" diff --git a/bkflow_dmn/data_model.py b/bkflow_dmn/data_model.py index 2202b47..43d00d6 100644 --- a/bkflow_dmn/data_model.py +++ b/bkflow_dmn/data_model.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import re from enum import Enum from typing import List, Union @@ -94,31 +95,52 @@ def hit_policy_value(self): @property def feel_exp_of_inputs(self): col_ids = [col.id for col in self.inputs.cols] - operator_prefix = { - ">": "", - "<": "", - "[": " in ", - "(": " in ", - } - or_operator = "," - result = [] for row in self.inputs.rows: if isinstance(row, str): result.append([row]) continue - row_exps = [] + row_result = [] for idx, unit in enumerate(row): pured_unit = unit.strip() if pured_unit == "": continue - unit_exps = [] - vals = [val.strip() for val in pured_unit.split(or_operator) if val.strip()] - for val in vals: - if val[0] in operator_prefix: - unit_exps.append(f"{col_ids[idx]}{operator_prefix[val[0]]}{val}") - else: - unit_exps.append(f"{col_ids[idx]}={val}") - row_exps.append(f'({" or ".join(unit_exps)})' if len(unit_exps) > 1 else unit_exps[0]) - result.append(row_exps) + handler = TableUnitHandler(unit_exp=pured_unit, col_id=col_ids[idx]) + row_result.append(handler.get_handled_exp()) + result.append(row_result) return result + + +class TableUnitHandler: + OPERATOR_PATTERNS = { + r"^>(.*?)": "", + r"^<(.*?)": "", + r"[\[\(](.*?)\.\.(.*?)[\]\)]": " in ", + r"\"(.*?)\"": "=", + r"-?\d+(\.\d+)?": "=", + r"true": "=", + r"false": "=", + r"null": "=", + } + OR_OPERATOR = "," + + def __init__(self, unit_exp: str, col_id: str): + self.exp = unit_exp + self.col_id = col_id + + def get_handled_exp(self): + exp_splices = [exp.strip() for exp in self.exp.split(self.OR_OPERATOR)] + union_patterns = "|".join(self.OPERATOR_PATTERNS.keys()) + + # 不能进行自动补全的情况 + if any([re.fullmatch(union_patterns, exp) is None for exp in exp_splices]): + return self.exp + + handled_exp_splices = [] + for exp in exp_splices: + for pattern, operator in self.OPERATOR_PATTERNS.items(): + if re.fullmatch(pattern, exp): + handled_exp_splices.append(f"{self.col_id}{operator}{exp}") + break + + return " or ".join(handled_exp_splices) diff --git a/release.md b/release.md index 2016cf6..015dfe2 100644 --- a/release.md +++ b/release.md @@ -1,5 +1,8 @@ # Release Notes +# 0.2.0 + - 单元格补全策略调整 + # 0.1.1 - 规则配置支持单行跨列规则 diff --git a/tests/data.py b/tests/data.py index ba22825..cc1403a 100644 --- a/tests/data.py +++ b/tests/data.py @@ -30,7 +30,6 @@ }, } - UNIQUE_SCORE_MULTI_ROW_FORMAT_TEST_TABLE = { "title": "testing", "hit_policy": "Unique", @@ -62,7 +61,6 @@ }, } - UNIQUE_TEST_DATA = [ (UNIQUE_SCORE_TEST_TABLE, {"score": 10, "sex": "boy"}, [{"level": "bad"}]), (UNIQUE_SCORE_TEST_TABLE, {"score": 80, "sex": "boy"}, [{"level": "good"}]), @@ -78,7 +76,6 @@ (UNIQUE_SCORE_MULTI_ROW_FORMAT_TEST_TABLE, {"score": 84, "sex": "girl"}, [{"level": "good++"}]), ] - ANY_SCORE_TEST_TABLE = { "title": "testing", "hit_policy": "Any", diff --git a/tests/test_single_table_decision.py b/tests/test_single_table_decision.py index a517976..56042fa 100644 --- a/tests/test_single_table_decision.py +++ b/tests/test_single_table_decision.py @@ -2,6 +2,7 @@ import pytest from bkflow_dmn.api import decide_single_table +from bkflow_dmn.data_model import SingleDecisionTable from tests.data import ( UNIQUE_TEST_DATA, ANY_TEST_DATA, @@ -34,6 +35,63 @@ def test_single_table_decision_strict_mode(single_table, facts, expected): assert result == expected +def test_single_table_inputs_autocomplete(): + rows = [ + [">=10"], + [">10"], + ["[1..2]"], + ["(1..2]"], + ["<=10"], + ["<10"], + ['"boy"'], + ["1"], + ["-1"], + ["1.2"], + ["-2.3"], + ["true"], + ["false"], + ["null"], + ["1 and 1"], + ["before(x, [2..3])"], + ["< 10, > 20"], + ["<10, >20"], + ] + TEST_TABLE = { + "title": "testing", + "hit_policy": "Unique", + "inputs": { + "cols": [{"id": "input"}], + "rows": rows, + }, + "outputs": { + "cols": [{"id": "output"}], + "rows": [[1]] * len(rows), + }, + } + table = SingleDecisionTable(**TEST_TABLE) + auto_completed_inputs = table.feel_exp_of_inputs + assert auto_completed_inputs == [ + ["input>=10"], + ["input>10"], + ["input in [1..2]"], + ["input in (1..2]"], + ["input<=10"], + ["input<10"], + ['input="boy"'], + ["input=1"], + ["input=-1"], + ["input=1.2"], + ["input=-2.3"], + ["input=true"], + ["input=false"], + ["input=null"], + ["1 and 1"], + ["before(x, [2..3])"], + ["input< 10 or input> 20"], + ["input<10 or input>20"], + ] + + @pytest.mark.parametrize( "single_table, facts, expected", test_data,