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 +)