If you are skilled in languages such as python, R or C# and start coding in SAS, you might miss some of the functions and methods you’re are so familiar with. Or perhaps you just would like a function that isn’t there, or reshape what's already in the SAS toolbox.
But hey, no worries - Proc FCMP to the rescue! It allows you to create and mimic your favorite functions with a few lines of code.
One of the string manipulation functions I really like in .Net (C#, VB) and miss in SAS is the String.Format method, so this article describes how the custom-built function FormatString I created works and how you can implement it yourself. It mimics the String.Format method and my examples use the Numeric and the Named placeholders, so there are actually two functions, FmtStrNum and FmtStrName. I will sometimes refer to them both as FormatString in the article.
Calling the function FmtStrNum that I created with FCMP…
FmtStrNum('Of all the great Prog rock bands {1}, {2}, {3}, {4} and {5}, my current favorite is {2}.', b1, b2, b3, b4, b5);
…would return this string with variables b1-b5 set with the band names.
Of all the great Prog Rock bands Aristocrats, MÖRGlbl, Cosmosquad, Neal Morse and Dream Theater, my current favorite is MÖRGlbl.
The function FmtStrNum uses numeric placeholders from {1} to {5}, and that works well in the above example. Further down you will see the function FmtStrName that uses named placeholders which is more suitable to that text string. Obviously, you can call these functions whatever you want.
FormatString reduces the need for concatenations which often is time-consuming and tedious to construct, but more importantly – FormatString is much easier to read and understand for you and others maintaining your code.
It could look like this the traditional way and compared to FmtStrNum it’s rather messy. Of course, we have the excellent CAT functions but it still results in a not so easy to read statement. FormatString is concatenation and replacement in one go, and easy to read as well.
Band = 'Of all the great Prog Rock bands ' !! strip(b1) !! ', ' !! strip(b2) !! ', ' !! strip(b3) !! ', ' !!
strip(b4) !! ' and ' !! strip(b5) !! ', my current favorite is ' !! strip(b2) !! '.';
We all know SAS is a very rich language with buckets of powerful functions, and FormatString use one of them with the addition of a few lines of code, to achieve this powerful string formatting function.
Custom-built function adds great value to your SAS programs. Easier to read, maintain and debug. You can replace big chunks of Case-When-Else SQL with a one-liner, having the logic handled much nicer in a function where you can do whatever is needed, not limited to what SQL allows. We can also write functions to replace SAS Formats which offers greater flexibility than PROC FORMAT. The FCMP documentation have some great examples on what you can do, but this article is about doing some of what the String.Method can do in e.g. C#.
This is the Proc FCMP code to build the function FmtStrNum.
The replacement is done with the SAS function transtrn
(translate string), but transtrn can only do one translation at a time, so you could say the FormatString is transtrn(
transtrn(
transtrn(
transtrn(
transtrn(
.
Proc fcmp outlib=WORK.funcs.dev;
/* The placeholders in FmtStrNum are identified using numbered indexes e.g. {1}, {2} etc. */
Function FmtStrNum(Text $, V1 $, V2 $, V3 $, V4 $, V5 $) $ 500;
array Values [5] V1 V2 V3 V4 V5;
array PlaceHolders [5] $ 3 '{1}' '{2}' '{3}' '{4}' '{5}';
length NewText $ 500;
NewText = text;
do i = 1 to dim(Values);
if Values[i] > '' then
NewText = transtrn(NewText, PlaceHolders[i], strip(Values[i]));
end;
Return(NewText);
Endsub;
Run;
Now try it yourself with this small program.
options cmplib=WORK.funcs;
data favorite;
length Band $ 300;
keep Band;
b1='Aristocrats';
b2='MÖRGlbl';
b3='Cosmosquad';
b4='Neal Morse';
b5='Dream Theater';
/* With FmtStrNum */
Band = FmtStrNum('Of all the great Prog Rock bands {1}, {2}, {3}, {4} and {5}, my current favorite is {2}.', b1, b2, b3, b4, b5);
output;
/* The Traditional way */
Band = 'Of all the great Prog Rock bands ' !! strip(b1) !! ', ' !! strip(b2) !! ', ' !! strip(b3) !! ', ' !! strip(b4) !! ' and ' !!
strip(b5) !! ', my current favorite is ' !! strip(b2) !! '.';
output;
run;
The content of the variable Band should now be:
Of all the great Prog Rock bands Aristocrats, MÖRGlbl, Cosmosquad, Neal Morse and Dream Theater, my current favorite is MÖRGlbl.
The next example use named placeholders which works better when we’re going to build a URL string; the FmtStrName function.
We just need to do small changes to the FCMP code from FmtStrNum:
This is the FCMP code to create the FmtStrName function.
Proc fcmp outlib=WORK.funcs.dev;
/* The placeholders in FmtStrName are identified using named indexes e.g. {band} or {guitar}. */
Function FmtStrName(Text $, PH1 $, V1 $, PH2 $, V2 $, PH3 $, V3 $, PH4 $, V4 $, PH5 $, V5 $) $ 500;
array Values [5] v1 v2 v3 v4 v5;
array PlaceHolders [5] ph1 ph2 ph3 ph4 ph5;
length NewText $ 500;
NewText = Text;
do i = 1 to dim(Values);
if Values[i] > '' then
NewText = transtrn(NewText, PlaceHolders[i], strip(Values[i]));
end;
Return(NewText);
Endsub;
Run;
Try it yourself with this program building a typical URL string we often see in Stored Processes or Viya Job Execution code.
options cmplib=WORK.funcs;
data url;
Length url $ 300;
keep url;
Artist = 'Guthrie Govan';
Band = 'The Aristocrats';
Guitar = '/images/Charvel Cooked Ash.png';
Rig = 666;
/* With FmtStrName */
url = FmtStrName('<a href="javascript: f.Rig.value=''{rig}''; f.submit();"><img title="Full rig rundown - {artist} with {band}." src="{guitar}"></a>',
'{rig}', put(Rig, best.),
'{artist}', Artist,
'{band}', Band,
'{guitar}', Guitar,
'', '');
output;
/* The Traditional way */
url = '<a href="javascript: f.Rig.value=' !! "'" !! strip(put(Rig, best.)) !! "'" !!
'; f.submit();"><img title="Full rig rundown - ' !! Strip(Artist) !! ' with ' !!
trim(Band) !! '." src="' !! Guitar !! '"></a>';
output;
run;
The content of the variable Url should now be:
<a href="javascript: f.Rig.value='666'; f.submit();"><img title="Full rig rundown - Guthrie Govan with The Aristocrats." src="/images/Charvel Cooked Ash.png"></a>
As you perhaps have noticed above, the last value pair is blank. This is due to that I have set up the FmtStrName to take 5 value pairs, and as Proc FCMP does not support variable arguments I have to pass on ‘ ’,’ ’ as the last pair. The same goes for the first example FmtStrNum; if I didn’t need the fifth parameter I would just leave ‘ ’ as last value. An alternative to this is to copy the FmtStrNum FCMP code and create an FmtStrNum2 taking just 2 parameters, an FmtStrNum3 taking 3 parameters and so on. This is how I have set it up for myself.
These both examples return a string 500 characters long, and you probably need to adjust this according to your needs. Setting local strings to 32k has a performance cost to it, so be sensible. For more details on FCMP and performance, read the blog post Removing repeated characters in SAS strings by Leonid Batkhan.
If you have any questions or suggestions please reply to this post or reach out via email.
@DanielRingqvist Cool article!
I don't know the original String.Format method. However, I am a huge fan of Proc FCMP and all the benefits you highlight.
The only note/suggestion I have is the constant number of arguments.
It is true that a variable number of arguments is not directly supported when calling user-defined functions and subroutines from the data step. However, this is true only for the data step. Consider the small example below
/* All Good */
proc fcmp outlib=work.f.f;
subroutine summin (t, x[*]) varargs;
outargs t;
t = 0;
do i = 1 to dim(x);
t = t + x[i];
end;
endsub;
call summin (t, 1, 2, 3);
put t=;
run;
options cmplib=(work.f);
/* Error */
data _null_;
call summin (t, 1, 2, 3);
put t=;
run;
It would be cool to be able to do this directly in the data step. While this is not directly possible, we can make it work. Though we need to jump a few hoops on the way.
As suggested in SAS Usage Note 41754, we can bundle the variables we want to pass to the function in an array and use the Varargs Argument in the FCMP Statement. However, since we're dealing with a variable number of character arguments this gives us an extra hoop to jump. If we simply use this technique blindly, we receive the error message "ERROR: The last argument to a VARARG function must be a numeric array.".
We can overcome this obstacle using an additional numeric, single-entry array and place it as the last argument in the Function Statement. The array is never used and serves merely to suppress the error message. This is the workaround presented in the SASWare Ballot here. I encourage readers to upvote the idea.
Putting all this together gives us the FCMP code below
proc fcmp outlib=work.f.f;
function fmtstrnum(text $, v[*] $, neverusednumarray[*]) varargs $ 500;
length newtext $ 500;
newtext = text;
do i = 1 to dim(v);
newtext = transtrn(newtext, cats('{', i, '}'), strip(v[i]));
end;
return(newtext);
endsub;
run;
Now, we can use the FmtStrNum Function with a variable number of arguments like below.
data test(keep=band);
b1='Aristocrats ';
b2='MÖRGlbl ';
b3='Cosmosquad ';
b4='Neal Morse ';
b5='Dream Theater';
array b b:;
array n{1} (1);
band = FmtStrNum('Of all the great Prog Rock bands {1}, {2}, {3}, {4} and {5}, my current favorite is {2}.', b, n);
output;
band = FmtStrNum('Of all the great Prog Rock bands {1}, {2} and {3}, my current favorite is {2}.', b, n);
output;
run;
The price: 2 additional array statements.
Again thank you for a nice article.
Regards Peter.
That's a super kool trick and I must admit I hadn't seen that note, and I'm also definitely not an expert on FCMP yet. Good info!
When I ran into the fact you have to pass on all arguments I thought about using arrays, but I had my mind set on doing something very clean.
I wanted to have the values in the call fully visible, no prepping of input strings, and I also wanted it to work in SQL and wherever a homegrown function might be used.
Having said that, I'm glad you shared this as if you're mostly writing data step code and you're fine with an array - then you don't need to create FmtStrNum2, FmtStrNum3, FmtStrNum4 and so on. Personally I'm fine with that. Haven't needed more than 5 items yet but that'll surely come😊
Great input Peter!
Registration is now open for SAS Innovate 2025 , our biggest and most exciting global event of the year! Join us in Orlando, FL, May 6-9.
Sign up by Dec. 31 to get the 2024 rate of just $495.
Register now!