From 444ffac0f34a66d9d894fce595088cf2f52f3e68 Mon Sep 17 00:00:00 2001
From: dcbot <_>
Date: Sun, 19 Jul 2026 23:17:12 +0100
Subject: [PATCH] fix: refactor of mf_wordsinstr1-xxx-str2 macros to support
000s of vals efficiently
---
base/mf_wordsinstr1andstr2.sas | 29 +++---
base/mf_wordsinstr1butnotstr2.sas | 31 +++----
tests/base/mf_wordsinstr1andstr2.test.sas | 94 +++++++++++++++++++
tests/base/mf_wordsinstr1butnotstr2.test.sas | 97 ++++++++++++++++++++
4 files changed, 216 insertions(+), 35 deletions(-)
diff --git a/base/mf_wordsinstr1andstr2.sas b/base/mf_wordsinstr1andstr2.sas
index 43949f5..bca241b 100644
--- a/base/mf_wordsinstr1andstr2.sas
+++ b/base/mf_wordsinstr1andstr2.sas
@@ -3,9 +3,12 @@
@brief Returns words that are in both string 1 and string 2
@details Compares two space separated strings and returns the words that are
in both.
+
+ If either string is empty, nothing is returned.
+
Usage:
- %put %mf_wordsInStr1andStr2(
+ %put %mf_wordsinstr1andstr2(
Str1=blah sss blaaah brah bram boo
,Str2= blah blaaah brah ssss
);
@@ -23,31 +26,23 @@
**/
-%macro mf_wordsInStr1andStr2(
+%macro mf_wordsinstr1andstr2(
Str1= /* string containing words to extract */
,Str2= /* used to compare with the extract string */
)/*/STORE SOURCE*/;
-%local count_base count_extr i i2 extr_word base_word match outvar;
+%local count_extr i extr_word outvar;
%if %length(&str1)=0 or %length(&str2)=0 %then %do;
- %put base string (str1)= &str1;
- %put compare string (str2) = &str2;
+ %put &sysmacroname: empty input string, nothing to compare;
%return;
%end;
-%let count_base=%sysfunc(countw(&Str2));
%let count_extr=%sysfunc(countw(&Str1));
%do i=1 %to &count_extr;
%let extr_word=%scan(&Str1,&i,%str( ));
- %let match=0;
- %do i2=1 %to &count_base;
- %let base_word=%scan(&Str2,&i2,%str( ));
- %if &extr_word=&base_word %then %let match=1;
- %end;
- %if &match=1 %then %let outvar=&outvar &extr_word;
+ %if %sysfunc(indexw(%superq(str2),%superq(extr_word)))>0 %then
+ %let outvar=&outvar &extr_word;
%end;
-
- &outvar
-
-%mend mf_wordsInStr1andStr2;
-
+/* send out the result without any surrounding whitespace */
+%do;&outvar%end;
+%mend mf_wordsinstr1andstr2;
diff --git a/base/mf_wordsinstr1butnotstr2.sas b/base/mf_wordsinstr1butnotstr2.sas
index 17b8ae3..6a3e4bd 100755
--- a/base/mf_wordsinstr1butnotstr2.sas
+++ b/base/mf_wordsinstr1butnotstr2.sas
@@ -6,9 +6,12 @@
Note - case sensitive!
+ If str1 is empty, nothing is returned. If str2 is empty, all the words in
+ str1 are returned.
+
Usage:
- %let x= %mf_wordsInStr1ButNotStr2(
+ %let x= %mf_wordsinstr1butnotstr2(
Str1=blah sss blaaah brah bram boo
,Str2= blah blaaah brah ssss
);
@@ -24,31 +27,23 @@
**/
-%macro mf_wordsInStr1ButNotStr2(
+%macro mf_wordsinstr1butnotstr2(
Str1= /* string containing words to extract */
,Str2= /* used to compare with the extract string */
)/*/STORE SOURCE*/;
-%local count_base count_extr i i2 extr_word base_word match outvar;
-%if %length(&str1)=0 or %length(&str2)=0 %then %do;
- %put base string (str1)= &str1;
- %put compare string (str2) = &str2;
+%local count_extr i extr_word outvar;
+%if %length(&str1)=0 %then %do;
+ %put &sysmacroname: str1 is empty, nothing to compare;
%return;
%end;
-%let count_base=%sysfunc(countw(&Str2));
%let count_extr=%sysfunc(countw(&Str1));
%do i=1 %to &count_extr;
%let extr_word=%scan(&Str1,&i,%str( ));
- %let match=0;
- %do i2=1 %to &count_base;
- %let base_word=%scan(&Str2,&i2,%str( ));
- %if &extr_word=&base_word %then %let match=1;
- %end;
- %if &match=0 %then %let outvar=&outvar &extr_word;
+ %if %sysfunc(indexw(%superq(str2),%superq(extr_word)))=0 %then
+ %let outvar=&outvar &extr_word;
%end;
-
- &outvar
-
-%mend mf_wordsInStr1ButNotStr2;
-
+/* send out the result without any surrounding whitespace */
+%do;&outvar%end;
+%mend mf_wordsinstr1butnotstr2;
diff --git a/tests/base/mf_wordsinstr1andstr2.test.sas b/tests/base/mf_wordsinstr1andstr2.test.sas
index 38a3c55..917df29 100644
--- a/tests/base/mf_wordsinstr1andstr2.test.sas
+++ b/tests/base/mf_wordsinstr1andstr2.test.sas
@@ -5,12 +5,17 @@
SAS Macros
@li mf_wordsinstr1andstr2.sas
@li mp_assert.sas
+ @li mp_assertscope.sas
**/
+/* basic test, with scope check */
+%mp_assertscope(SNAPSHOT)
%let x=%mf_wordsinstr1andstr2(str1=xx DOLLAR x $CHAR xxx W MONNAME
,str2=DOLLAR $CHAR W MONNAME xxxxxx
);
+%mp_assertscope(COMPARE,ignorelist=x)
+
%mp_assert(
iftrue=(
"&x"="DOLLAR $CHAR W MONNAME"
@@ -18,3 +23,92 @@
desc=Checking basic string,
outds=work.test_results
)
+
+/* word boundary - var should not match var1 or var10 */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1andstr2(str1=var1 var10,str2=var var1)"="var1"
+ ),
+ desc=Checking word boundaries,
+ outds=work.test_results
+)
+
+/* case sensitivity - dollar does not match DOLLAR */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1andstr2(str1=DOLLAR dollar,str2=DOLLAR)"="DOLLAR"
+ ),
+ desc=Checking case sensitivity,
+ outds=work.test_results
+)
+
+/* duplicate words in str1 are preserved */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1andstr2(str1=a a b,str2=a)"="a a"
+ ),
+ desc=Checking duplicate words,
+ outds=work.test_results
+)
+
+/* when no words match, nothing is returned */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1andstr2(str1=a b,str2=c d)"=""
+ ),
+ desc=Checking empty result,
+ outds=work.test_results
+)
+
+/* when str1 is empty, nothing is returned */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1andstr2(str1=,str2=a b)"=""
+ ),
+ desc=Checking empty str1,
+ outds=work.test_results
+)
+
+/* when str2 is empty, nothing is returned */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1andstr2(str1=a b,str2=)"=""
+ ),
+ desc=Checking empty str2,
+ outds=work.test_results
+)
+
+/* build strings containing 1000 variables */
+/* str2 is kept to 100 words to avoid excessive macro iterations */
+data _null_;
+ length str1 str2 $32767;
+ do i=1 to 1000;
+ word=cats('var',i);
+ str1=catx(' ',str1,word);
+ if mod(i,10)=0 then str2=catx(' ',str2,word);
+ end;
+ call symputx('str1',str1);
+ call symputx('str2',str2);
+run;
+
+%mp_assertscope(SNAPSHOT)
+%let result=%mf_wordsinstr1andstr2(str1=&str1,str2=&str2);
+%mp_assertscope(COMPARE,ignorelist=result)
+
+%let count=%sysfunc(countw(&result));
+
+%mp_assert(
+ iftrue=(
+ "&count"="100"
+ ),
+ desc=Checking 1000 variable string returns 100 words,
+ outds=work.test_results
+)
+
+%mp_assert(
+ iftrue=(
+ "&result"="&str2"
+ ),
+ desc=Checking 1000 variable string content,
+ outds=work.test_results
+)
diff --git a/tests/base/mf_wordsinstr1butnotstr2.test.sas b/tests/base/mf_wordsinstr1butnotstr2.test.sas
index 48fd6fc..cb7a9da 100644
--- a/tests/base/mf_wordsinstr1butnotstr2.test.sas
+++ b/tests/base/mf_wordsinstr1butnotstr2.test.sas
@@ -5,12 +5,17 @@
SAS Macros
@li mf_wordsinstr1butnotstr2.sas
@li mp_assert.sas
+ @li mp_assertscope.sas
**/
+/* basic test, with scope check */
+%mp_assertscope(SNAPSHOT)
%let x=%mf_wordsinstr1butnotstr2(str1=xx DOLLAR x $CHAR xxx W MONNAME
,str2=ff xx x xxx xxxxxx
);
+%mp_assertscope(COMPARE,ignorelist=x)
+
%mp_assert(
iftrue=(
"&x"="DOLLAR $CHAR W MONNAME"
@@ -18,3 +23,95 @@
desc=Checking basic string,
outds=work.test_results
)
+
+/* word boundary - var1 should not match var10 or var100 */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1butnotstr2(str1=var1 var10 var100,str2=var1)"
+ ="var10 var100"
+ ),
+ desc=Checking word boundaries,
+ outds=work.test_results
+)
+
+/* case sensitivity - dollar does not match DOLLAR */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1butnotstr2(str1=DOLLAR dollar,str2=DOLLAR)"="dollar"
+ ),
+ desc=Checking case sensitivity,
+ outds=work.test_results
+)
+
+/* duplicate words in str1 are preserved */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1butnotstr2(str1=a a b a,str2=b)"="a a a"
+ ),
+ desc=Checking duplicate words,
+ outds=work.test_results
+)
+
+/* when all words match, nothing is returned */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1butnotstr2(str1=a b,str2=b a)"=""
+ ),
+ desc=Checking empty result,
+ outds=work.test_results
+)
+
+/* when str1 is empty, nothing is returned */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1butnotstr2(str1=,str2=a b)"=""
+ ),
+ desc=Checking empty str1,
+ outds=work.test_results
+)
+
+/* when str2 is empty, all of str1 is returned */
+%mp_assert(
+ iftrue=(
+ "%mf_wordsinstr1butnotstr2(str1=a b c,str2=)"="a b c"
+ ),
+ desc=Checking empty str2 returns all words,
+ outds=work.test_results
+)
+
+/* build strings containing 1000 variables */
+/* str2 is kept to 100 words to avoid excessive macro iterations */
+data _null_;
+ length str1 str2 expected $32767;
+ do i=1 to 1000;
+ word=cats('var',i);
+ str1=catx(' ',str1,word);
+ if mod(i,10)=0 then str2=catx(' ',str2,word);
+ else expected=catx(' ',expected,word);
+ end;
+ call symputx('str1',str1);
+ call symputx('str2',str2);
+ call symputx('expected',expected);
+run;
+
+%mp_assertscope(SNAPSHOT)
+%let result=%mf_wordsinstr1butnotstr2(str1=&str1,str2=&str2);
+%mp_assertscope(COMPARE,ignorelist=result)
+
+%let count=%sysfunc(countw(&result));
+
+%mp_assert(
+ iftrue=(
+ "&count"="900"
+ ),
+ desc=Checking 1000 variable string returns 900 words,
+ outds=work.test_results
+)
+
+%mp_assert(
+ iftrue=(
+ "&result"="&expected"
+ ),
+ desc=Checking 1000 variable string content,
+ outds=work.test_results
+)