From 5d232685aaca16a72aa8ce3ffd5bfa2a0e7f22c9 Mon Sep 17 00:00:00 2001 From: "Quiblat, Jan Michael" Date: Wed, 22 Jul 2026 12:35:32 -0400 Subject: [PATCH 1/2] Does not start or end with operators --- .../check_operators/dataframe_operators.py | 10 ++++ resources/schema/rule-merged/Operator.json | 20 +++++++ resources/schema/rule/Operator.json | 10 ++++ resources/schema/rule/Operator.md | 24 ++++++++ .../test_string_comparison.py | 58 +++++++++++++++++++ 5 files changed, 122 insertions(+) diff --git a/cdisc_rules_engine/check_operators/dataframe_operators.py b/cdisc_rules_engine/check_operators/dataframe_operators.py index 66d2d8fdf..926a29d7d 100644 --- a/cdisc_rules_engine/check_operators/dataframe_operators.py +++ b/cdisc_rules_engine/check_operators/dataframe_operators.py @@ -849,6 +849,11 @@ def starts_with(self, other_value): results = self.value[target].str.startswith(comparison_data) return results + @log_operator_execution + @type_operator(FIELD_DATAFRAME) + def does_not_start_with(self, other_value): + return ~self.starts_with(other_value) + @log_operator_execution @type_operator(FIELD_DATAFRAME) def ends_with(self, other_value): @@ -862,6 +867,11 @@ def ends_with(self, other_value): results = self.value[target].str.endswith(comparison_data) return results + @log_operator_execution + @type_operator(FIELD_DATAFRAME) + def does_not_end_with(self, other_value): + return ~self.ends_with(other_value) + @log_operator_execution @type_operator(FIELD_DATAFRAME) def has_equal_length(self, other_value: dict): diff --git a/resources/schema/rule-merged/Operator.json b/resources/schema/rule-merged/Operator.json index e1b988e6a..c195f0e73 100644 --- a/resources/schema/rule-merged/Operator.json +++ b/resources/schema/rule-merged/Operator.json @@ -184,6 +184,16 @@ "required": ["operator", "value"], "type": "object" }, + { + "properties": { + "operator": { + "const": "does_not_end_with", + "markdownDescription": "\nSubstring matching\n\n> DOMAIN not ending with 'FOOBAR'\n\n```yaml\n- name: \"DOMAIN\"\n operator: \"does_not_end_with\"\n value: \"FOOBAR\"\n```\n" + } + }, + "required": ["operator", "value"], + "type": "object" + }, { "properties": { "operator": { @@ -792,6 +802,16 @@ "required": ["operator", "value"], "type": "object" }, + { + "properties": { + "operator": { + "const": "does_not_start_with", + "markdownDescription": "\nSubstring matching\n\n> DOMAIN not beginning with 'AP'\n\n```yaml\n- name: \"DOMAIN\"\n operator: \"does_not_start_with\"\n value: \"AP\"\n```\n" + } + }, + "required": ["operator", "value"], + "type": "object" + }, { "properties": { "operator": { diff --git a/resources/schema/rule/Operator.json b/resources/schema/rule/Operator.json index 021e79f89..63c9c6435 100644 --- a/resources/schema/rule/Operator.json +++ b/resources/schema/rule/Operator.json @@ -102,6 +102,11 @@ "required": ["operator", "value"], "type": "object" }, + { + "properties": { "operator": { "const": "does_not_end_with" } }, + "required": ["operator", "value"], + "type": "object" + }, { "properties": { "operator": { "const": "equal_to" }, @@ -432,6 +437,11 @@ "required": ["operator", "value"], "type": "object" }, + { + "properties": { "operator": { "const": "does_not_start_with" } }, + "required": ["operator", "value"], + "type": "object" + }, { "properties": { "operator": { "const": "suffix_equal_to" } }, "required": ["operator", "suffix", "value"], diff --git a/resources/schema/rule/Operator.md b/resources/schema/rule/Operator.md index b05e9a22b..4d9bf581f 100644 --- a/resources/schema/rule/Operator.md +++ b/resources/schema/rule/Operator.md @@ -259,6 +259,18 @@ Substring matching value: "AP" ``` +### does_not_start_with + +Substring matching + +> DOMAIN not beginning with 'AP' + +```yaml +- name: "DOMAIN" + operator: "does_not_start_with" + value: "AP" +``` + ### ends_with Substring matching @@ -271,6 +283,18 @@ Substring matching value: "FOOBAR" ``` +### does_not_end_with + +Substring matching + +> DOMAIN not ending with 'FOOBAR' + +```yaml +- name: "DOMAIN" + operator: "does_not_end_with" + value: "FOOBAR" +``` + ### prefix_equal_to True if the `prefix` number of characters beginning a string in `name` match the string in `value` diff --git a/tests/unit/test_check_operators/test_string_comparison.py b/tests/unit/test_check_operators/test_string_comparison.py index c4a3d7fbc..b44927d0a 100644 --- a/tests/unit/test_check_operators/test_string_comparison.py +++ b/tests/unit/test_check_operators/test_string_comparison.py @@ -309,6 +309,32 @@ def test_starts_with(data, comparator, dataset_type, expected_result): assert result.equals(df.convert_to_series(expected_result)) +@pytest.mark.parametrize( + "data,comparator,dataset_type,expected_result", + [ + ( + {"target": ["Att", "Btt", "Ctt"], "VAR2": ["A", "B", "D"]}, + "VAR2", + PandasDataset, + [False, False, True], + ), + ( + {"target": ["Att", "Btt", "Ctt"], "VAR2": ["A", "B", "D"]}, + "VAR2", + DaskDataset, + [False, False, True], + ), + ], +) +def test_does_not_start_with(data, comparator, dataset_type, expected_result): + df = dataset_type.from_dict(data) + dataframe_type = DataframeType({"value": df}) + result = dataframe_type.does_not_start_with( + {"target": "target", "comparator": comparator} + ) + assert result.equals(df.convert_to_series(expected_result)) + + @pytest.mark.parametrize( "data,comparator,dataset_type,expected_result", [ @@ -333,6 +359,38 @@ def test_ends_with(data, comparator, dataset_type, expected_result): assert result.equals(df.convert_to_series(expected_result)) +@pytest.mark.parametrize( + "data,comparator,dataset_type,expected_result", + [ + ( + {"target": ["Att", "Btt", "Ctt"], "VAR2": ["A", "Bd", "lll"]}, + "VAR2", + DaskDataset, + [True, True, True], + ), + ( + {"target": ["Att", "Btt", "Ctt"], "VAR2": ["A", "Bd", "lll"]}, + "Att", + PandasDataset, + [False, True, True], + ), + ( + {"target": ["A2", "Btt", "Ctt2"], "VAR2": ["2", "3", "2"]}, + "VAR2", + PandasDataset, + [False, True, False], + ), + ], +) +def test_does_not_end_with(data, comparator, dataset_type, expected_result): + df = dataset_type.from_dict(data) + dataframe_type = DataframeType({"value": df}) + result = dataframe_type.does_not_end_with( + {"target": "target", "comparator": comparator} + ) + assert result.equals(df.convert_to_series(expected_result)) + + @pytest.mark.parametrize( "data,comparator,dataset_type,expected_result", [ From e3e2b2bbec90a96d1c72929f9abc18b595746f5e Mon Sep 17 00:00:00 2001 From: "Quiblat, Jan Michael" Date: Wed, 22 Jul 2026 12:44:02 -0400 Subject: [PATCH 2/2] fix lint error --- tests/unit/test_check_operators/test_string_comparison.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_check_operators/test_string_comparison.py b/tests/unit/test_check_operators/test_string_comparison.py index b44927d0a..3f0417032 100644 --- a/tests/unit/test_check_operators/test_string_comparison.py +++ b/tests/unit/test_check_operators/test_string_comparison.py @@ -389,7 +389,7 @@ def test_does_not_end_with(data, comparator, dataset_type, expected_result): {"target": "target", "comparator": comparator} ) assert result.equals(df.convert_to_series(expected_result)) - + @pytest.mark.parametrize( "data,comparator,dataset_type,expected_result",