text:
/*====================================================================
Program Name            : get_permutations.sas
Purpose                 : Gets all the permutations (combinations) of
                          values from an input list of data.
SAS Version             : SAS 9.3
Input Data              : SAS input dataset or list of items
Output Data             : SAS output dataset

Macros Called           : parmv, seplist

This macro is based on original work by
Francis Joseph (Joe) Kelley documented in the SUGI paper
https://support.sas.com/resources/papers/proceedings/proceedings/sugi23/Posters/p177.pdf

Permission has been granted by Joe to use his original ideas for this macro.

Originally Written by   : Scott Bass
Date                    : 14MAY2020
Program Version #       : 1.0

======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    : Original version

====================================================================*/

/*--------------------------------------------------------------------
Usage:

%get_permutations(list=b a c f d e)

Creates the _get_permutations_ dataset (default output dataset name)
from the input list of values,
at a depth of 1 level (default).

======================================================================

%get_permutations(list=b a c f d e,level=1)
%get_permutations(list=b a c f d e,level=2)
%get_permutations(list=b a c f d e,level=3)

Creates the _get_permutations_ dataset
from the input list of values,
at a depth of 1, 2, and 3 levels respectively.

======================================================================

%get_permutations(list=b a c f d e,min_level=1,max_level=1)
%get_permutations(list=b a c f d e,min_level=2,max_level=2)
%get_permutations(list=b a c f d e,min_level=3,max_level=3)

Same as above, but explicitly setting min_level and max_level.

======================================================================

%get_permutations(list=b a c f d e,min_level=2,max_level=4)
%get_permutations(list=b a c f d e,min_level=3,max_level=5)

Creates the _get_permutations_ dataset
from the input list of values,
at a depth of 2-4 and 3-5 respectively.

======================================================================

%get_permutations(list=b a c f d e,level=10)

Creates the _get_permutations_ dataset (default output dataset name)
from the input list of values,
at a depth of number of items level.

Since min_level and max_level were not specified,
they are internally set to level, i.e. 10.

But since max_level is greater than the number of items,
it is set to the number of items in the list, i.e. 6.

But since min_level is now greater than max_level, it is set to
max_level, i.e. 6.

======================================================================

%get_permutations(list=b a c f d e,min_level=3,level=10)
%get_permutations(list=b a c f d e,min_level=3,max_level=10)

Same as above, but since min_level was explicitly specified,
and it is less than the (reset) max_level (6), it remains set as 3,
and the result is a "slice" of the permutations from 3-6.

You can use this technique (a large value for level or max_level)
to create a "slice" of permutations from min_value to the number of items.

======================================================================

%get_permutations(list=b a c f d e,level=0)

Creates the _get_permutations_ dataset (default output dataset name)
from the input list of values,
at all possible depth levels, i.e. all possible permutations.

======================================================================

%get_permutations(list=bb a ccc ffffff dddd eeeee a eeeee ccc)

Creates the _get_permutations_ dataset
from the input list of values,
at a depth of 1 level (default).

The code removes the duplicate values in the list and displays
a warning message in the log.

======================================================================

proc sql noprint;
   create table _contents_ as
   select name
   from dictionary.columns
   where libname='SASHELP' and memname='SHOES'
   order by varnum;
quit;

%get_permutations(data=_contents_,var=name,level=1,print=Y)
%get_permutations(data=_contents_,var=name,level=2,print=Y)
%get_permutations(data=_contents_,var=name,level=3,print=Y)

Creates the _get_permutations_ dataset (default output dataset name)
from the input list of column names,
at a depth of 1, 2, and 3 levels respectively,
and prints the output dataset.

======================================================================

proc sql noprint;
   create table _contents_ as
   select name
   from dictionary.columns
   where libname='SASHELP' and memname='SHOES'
   order by varnum;
quit;

%get_permutations(data=_contents_,var=name,level=0,print=Y)

Derives every permutation possible,
across all possible levels (1 to number of items)
from the column list of sashelp.shoes,
and prints the output dataset.

======================================================================

proc sql noprint;
   create table _contents_ as
   select name
   from dictionary.columns
   where libname='SASHELP' and memname='PRICEDATA'
   order by varnum;
quit;

%get_permutations(data=_contents_,var=name,level=1,out=permutations,print=Y)
%get_permutations(data=_contents_,var=name,level=2,out=permutations,print=Y)
%get_permutations(data=_contents_,var=name,level=3,out=permutations,print=Y)

* do not print these! ;
%get_permutations(data=_contents_,var=name,level=4,out=permutations)
%get_permutations(data=_contents_,var=name,level=5,out=permutations)

DO NOT run this with level=0!
The permutations will be in the 10's of millions
and will take forever to run!

Creates the permutations output dataset,
from the input list of column names,
at a depth of 1, 2, 3, 4, and 5 levels respectively.

This example demonstrates how the list of permutations increases
substantially with an increase in the number of items and the
depth level.

Note however that the distribution is analogous to a bell curve.
As the level passes the midpoint of the number of items,
the number of permutations decreases.

======================================================================

Error Testing:

%get_permutations()

Either DATA or LIST must be specified.

%get_permutations(data=doesnotexist)

If DATA is specified it must be an existing dataset or view
(including RDBMS tables).

%get_permutations(data=sashelp.class,var=foo)

If DATA is specified, then VAR must also be specified
and must exist in the DATA dataset or view.

%get_permutations(data=sashelp.class,var=name,out=)

OUT is a required parameter and must be a valid output dataset name.

%get_permutations(data=sashelp.class,var=name,level=)

LEVEL is a required parameter.

%get_permutations(data=sashelp.class,var=name,level=-1)

LEVEL must be a NONNEGATIVE number.

%get_permutations(data=sashelp.class,var=name,min_level=0)

MIN_LEVEL must be a POSITIVE number.

%get_permutations(data=sashelp.class,var=name,max_level=0)

MAX_LEVEL must be a POSITIVE number.

%get_permutations(data=sashelp.class,var=name,min_level=5,max_level=3)

MIN_LEVEL must less than or equal to MAX_LEVEL.

----------------------------------------------------------------------
Notes:

This macro creates working datasets with a naming convention
using leading and trailing underscores, i.e. _<dataset name>_.
If your session also uses this naming convention for your datasets
there is a slight chance of dataset name collision.

The default output dataset name is _<name of macro>_, i.e.
_get_permutations_.

Setting LEVEL is the equivalent of setting MIN_LEVEL and MAX_LEVEL
to that value.
This will create an output dataset of that depth level only.

Setting LEVEL=0 is the equivalent of setting MIN_LEVEL=1 and
MAX_LEVEL to the number of items in the source dataset.
This will create an output dataset of all possible depth levels,
i.e. all possible permutations.  This can create a SIGNIFICANT
number of permutations.

If both MIN_LEVEL and MAX_LEVEL are set, then LEVEL is ignored.
This will create an output dataset of those specified depth levels,
i.e. a "slice" of all possible permutations.

--------------------------------------------------------------------*/

%macro get_permutations
/*--------------------------------------------------------------------
Gets all the permutations (combinations) of values
from an input list of data.
--------------------------------------------------------------------*/
(DATA=         /* Input dataset (Opt).                              */
               /* If not specified, the LIST parameter must be      */
               /* specified.                                        */
,OUT=work._get_permutations_
               /* Output dataset (REQ).                             */
               /* The default value is work._get_permutations_.     */
,VAR=          /* Variable containing the list of items from which  */
               /* the permutations are generated (Opt).             */
               /* If the DATA parameter is specified,               */
               /* the VAR parameter is also required.               */
,LIST=         /* A space separated list of items from which the    */
               /* permutations are generated (Opt).                 */
               /* If specified, the DATA and VAR parameters are     */
               /* ignored.                                          */
,LEVEL=1       /* Desired depth level of permutations to generate   */
               /* (REQ).  Default value is 1.                       */
               /* If LEVEL=0, then all column permutations will     */
               /* be generated.  This can result in a significant   */
               /* number of permutations!                           */
,MIN_LEVEL=    /* Starting depth level of permutations to generate  */
               /* (Opt).                                            */
,MAX_LEVEL=    /* Maximum depth level of permutations to generate   */
               /* (Opt).                                            */
,PRINT=N       /* Print the OUT dataset to the Results window? (REQ)*/
               /* Default value is NO.  Valid values are:           */
               /* 0 1 OFF N NO F FALSE and ON Y YES T TRUE          */
               /* OFF N NO F FALSE and ON Y YES T TRUE              */
               /* (case insensitive) are acceptable aliases for     */
               /* 0 and 1 respectively.                             */
);

%local macro parmerr;
%local line dsid varnum dups sqlobs num_items max_len start stop i lev;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(DATA,         _req=0,_words=0,_case=N)
%parmv(OUT,          _req=1,_words=1,_case=N)  /* words=1 allows ds options */
%parmv(VAR,          _req=0,_words=0,_case=N)
%parmv(LIST,         _req=0,_words=1,_case=N)
%parmv(LEVEL,        _req=1,_words=0,_case=N,_val=NONNEGATIVE)
%parmv(MIN_LEVEL,    _req=0,_words=0,_case=N,_val=POSITIVE)
%parmv(MAX_LEVEL,    _req=0,_words=0,_case=N,_val=POSITIVE)
%parmv(PRINT,        _req=1,_words=0,_case=U,_val=0 1)

%if (&parmerr) %then %goto quit;

%let line=%sysfunc(repeat(=,71));

%* Additional error checking ;

%* If MIN_LEVEL or MAX_LEVEL are not specified set them to LEVEL ;
%if (&min_level eq ) %then %let min_level=&level;
%if (&max_level eq ) %then %let max_level=&level;

%* MIN_LEVEL must be less than or equal to MAX_LEVEL ;
%if (&min_level gt &max_level) %then %do;
   %parmv(_msg=MIN_LEVEL (&min_level) must be less than or equal to MAX_LEVEL (&max_level))
   %goto quit;
%end;

%* either DATA or LIST must be specified ;
%if (%superq(data)%superq(list) eq ) %then %do;
   %parmv(_msg=Either DATA or LIST must be specified)
   %goto quit;
%end;

%* If DATA was specified does the dataset exist? ;
%if (%superq(data) ne ) %then %do;
   %if ^(%sysfunc(exist(&data,data)) or
         %sysfunc(exist(&data,view))) %then %do;
      %parmv(_msg=&data does not exist)
      %goto quit;
   %end;

   %* If DATA was specified then VAR must also be specified ;
   %if (%superq(var) eq ) %then %do;
      %parmv(_msg=If DATA is specified then VAR must also be specified.)
      %goto quit;
   %end;

   %* Does VAR exist in the specified dataset ;
   %let dsid=%sysfunc(open(&data));
   %if (&dsid le 0) %then %do;
      %parmv(_msg=Unable to open &data dataset)
      %goto quit;
   %end;
   %let varnum = %sysfunc(varnum(&dsid,&var));
   %if (&varnum le 0) %then %do;
      %parmv(_msg=&var does not exist in &data)
      %let dsid=%sysfunc(close(&dsid));
      %goto quit;
   %end;
   %let dsid=%sysfunc(close(&dsid));
%end;

%* End of additional error checking ;

%* If a list was passed in convert to a dataset ;
%if (%superq(list) ne ) %then %do;
   data work._list_ (compress=no);
      length item $200;
      do item=%seplist(&list,nest=q);
         output;
      end;
   run;
   %let data=work._list_;
   %let var=item;
%end;

* check for duplicate items.  get maximum item length. ;
proc sql noprint;
   %* check for dups ;
   select &var into :dups separated by ' '
   from &data
   group by &var
   having count(0) > 1;

%if (&sqlobs ne 0) %then %do;
   %put &line;
   %put %str(WAR)NING: Duplicate values found in &data: &dups..;
   %put %str(WAR)NING: De-duplicating data.;
   %put &line;

   * we cannot (easily) use select distinct and keep data order due to the vagaries of SQL ;
   * so use an alternative approach to dedup the data ;

   %* I tried creating a view but it does not give the correct results ;
   %* I think it is due to the runtime execution of the monotonic() function ;
   %* when used within a view that is aggregating on a grouping variable ;

   * create a table of the source dataset with a sorting column to retain the data order ;
   create table work._varnum_ as
   select &var, monotonic() as varnum
   from &data;

   create table work._dedup_ as
   select &var
   from _varnum_
   group by &var
   having varnum=min(varnum)
   order by varnum;

   %let data=work._dedup_;
%end;

   %* get maximum item length ;
   select max(length(&var)) into :max_len trimmed from &data;
quit;

%let num_items=%nobs(&data);

%* Now that we have NUM_ITEMS, process for LEVEL=0 ;
%if (&level eq 0) %then %do;
   %let min_level=1;
   %let max_level=&num_items;
%end;

%* If MAX_LEVEL is greater than the NUM_ITEMS set it to NUM_ITEMS ;
%if (&max_level gt &num_items) %then %do;
   %put &line;
   %put %str(NO)TE: Specified MAX_LEVEL (&max_level) is greater than the number of items (&num_items).;
   %put %str(NO)TE: Resetting MAX_LEVEL from &level to &num_items..;
   %put &line;
   %let max_level=&num_items;
%end;

%* If MIN_LEVEL is greater than MAX_LEVEL set it to MAX_LEVEL ;
%if (&min_level gt &max_level) %then %do;
   %let min_level=&max_level;
%end;

* get the permutations ;
proc transpose data=&data out=work._items_ (compress=no drop=_name_) prefix=_____;
   var &var;
run;

%macro permutations(out);
   %let start=1;
   %let stop=%eval(&num_items - &level);
   data &out;
      set work._items_;
      length item0 $1000;
      array _____{*} _____:;
      array items{*} $&max_len item1 - item&level;
      keep item:;

      %do i = 1 %to &level;
         %let stop = %eval(&stop + 1);
         do idx&i = &start to &stop;
         %let start = %str(%(idx&i + 1%)); %* yes this is correct, do not change this! ;
      %end;
      %do i = 1 %to &level;
         items{&i} = _____{idx&i};
      %end;
      item0=catx(' ',of item1 - item&level);
      output;
      %do i = 1 %to &level;
         end;
      %end;
   run;
%mend;

%do level=&min_level %to &max_level;
   %permutations(work._permutations_%sysfunc(putn(&level,z2.))_)
%end;

%* create consolidated dataset ;
data &out;
   set
   %do level=&min_level %to &max_level;
      work._permutations_%sysfunc(putn(&level,z2.))_
   %end;
   ;
   /* rowcnt+1 */
run;

proc datasets lib=work nowarn nolist;
   delete _permutations_:;
quit;

%if (&print) %then %do;
   title "Permutations for &data";
   proc print data=&out;
   run;
   title;
%end;

%quit:

%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : symget.sas
Purpose                 : Get value for global macro variable that is
                          hidden by local macro variable.
SAS Version             : SAS 9.1.3
Input Data              : None
Output Data             : None

Macros Called           : parmv, seplist

Originally Written by   : Tom Abernathy (via SAS-L posting)
Date                    : 08APR2011
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

Based on original work by by Tom Abernathy of Pfizer Pharmaceuticals
in reply to a posting I made to the SAS-L listserver.
Used with permission.

=======================================================================

Modification History    :

Programmer              : Scott Bass
Date                    : 08APR2011
Change/reason           : This macro was supplied by Tom Abernathy of
                          Pfizer Pharmaceuticals in reply to a posting I
                          made to the SAS-L listserver.  I've edited it
                          slightly.  Macro used with permission from Tom
                          Abernathy.
Program Version #       : 1.1

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

%* Allow Global Macro Variable (GMV) to serve as default value ;
%local mvar;
%if %bquote(&MVAR)= %then %let mvar=%symget(mvar);

%* Allow both GMV and parameter style macro calls ;
%macro aesumm(dsin=,dsut=,sev=);
   %local macro;
   %let macro=&sysmacroname;
   %if %bquote(&dsin=) %then %let dsnin=%symget(dsnin,exclude=&macro);
   %if %bquote(&dsut=) %then %let dsnut=%symget(dsnut,exclude=&macro);
   %if %bquote(&sev=)  %then %let sev  =%symget(sev,  exclude=&macro);
   ....
%mend aesumm;

-----------------------------------------------------------------------
Notes:

- Default to finding GLOBAL macro variable.
- Set SYSRC=1 when macro variable not found.
- Macro will call itself recursively to eliminate extra trailing blanks
  pulled from SASHELP.VMACRO.

---------------------------------------------------------------------*/

%macro symget
/*---------------------------------------------------------------------
Get value for macro variable that is hidden by local macro variable
---------------------------------------------------------------------*/
(MVAR          /* Macro variable name                                */
,INCLUDE=      /* Space delimited list of macro scopes to search     */
,EXCLUDE=      /* Space delimited list of macro scopes to ignore     */
,RECURSE=N     /* Used internally by the macro                       */
);

%local macro parmerr did rc where scope name value offset;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(MVAR,         _req=1,_words=0,_case=N)
%parmv(INCLUDE,      _req=0,_words=1,_case=U)
%parmv(EXCLUDE,      _req=0,_words=1,_case=U)
%parmv(RECURSE,      _req=1,_words=0,_case=U,_val=0 1)

%if (&parmerr) %then %goto quit;

%if (&recurse) %then %do;
%*----------------------------------------------------------------------
Open SASHELP.VMACRO and link dataset variables to macro variables.
If a match is found then expand VALUE as the result of the macro call.
Loop until all observations are read or OFFSET=0 indicates that the
start of another instance of the same variable has begun.
Close SASHELP.VMACRO.
-----------------------------------------------------------------------;
   %let where=%upcase(name="&mvar" and scope="&include");
   %let did=%sysfunc(open(sashelp.vmacro(where=(&where))));
   %syscall set(did);
   %if 0=%sysfunc(fetch(&did)) %then %do;
      %do %until(%sysfunc(fetch(&did)) or 0=&offset);&value.%end;
   %end;
   %let rc=%sysfunc(close(&did));
%end;
%else
%if %bquote(&mvar)^= %then %do;
%*----------------------------------------------------------------------
Set scope to GLOBAL when no criteria specified. Exclude this macro.
Open the SASHELP.VMACRO and link dataset variables to macro variables.
Fetch first observation to get SCOPE and NAME. Close SASHELP.VMACRO.
-----------------------------------------------------------------------;
   %if %length(&include) %then
      %let where=scope in (%seplist(%upcase(&include),nest=QQ));
   %else
   %if 0=%length(&exclude) %then
      %let where=scope='GLOBAL';
   %else
      %let where=scope not in (%seplist(%upcase(&macro &exclude),nest=QQ));
   %let where=name="%upcase(&mvar)" and &where;
   %let did=%sysfunc(open(sashelp.vmacro(where=(&where))));
   %syscall set(did);
   %let rc=%sysfunc(fetch(&did));
   %let did=%sysfunc(close(&did));
   %if (0=&rc) %then %do;
%*----------------------------------------------------------------------
Found a variable so get value from recursive call to the macro.
Expand value as result of macro call.
-----------------------------------------------------------------------;
    %let value=%&macro(&name,include=&scope,recurse=Y);
&value.
  %end;
%end;

%*----------------------------------------------------------------------
Set SYSRC=1 to indicate macro variable not found.
-----------------------------------------------------------------------;
%let sysrc=%eval(%bquote(&name)=);

%quit:

%mend;

repo_name: scottbass/SAS

repo_language: SAS

mime_type: text/plain


text:
/*====================================================================
Program Name            : compare.sas
Purpose                 : PROC COMPARE either two datasets or
                          two libraries
SAS Version             : SAS 9.1.3
Input Data              : Either Base and Compare datasets or
                          Base and Compare libraries
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 21JAN2009
Program Version #       : 1.0

======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    : Original version

====================================================================*/

/*--------------------------------------------------------------------
Usage:

options nocenter mprint ls=max;

data base;
   set sashelp.class;
   label age="Age In Years";
run;
proc sort;
   by name;
run;

data comp;
   set sashelp.class;
   if name="John" then age=99;
   drop sex;
   label age="Age";
run;
proc sort;
   by name;
run;

%compare(base=base,comp=comp,by=name)

Compares the base and compare datasets, reporting on differences for
both variables and observations.

======================================================================

* use the SPDE engine to create temporary libraries ;
libname lib_base spde "%sysfunc(pathname(work))" temp=yes;
libname lib_comp spde "%sysfunc(pathname(work))" temp=yes;

proc copy inlib=sashelp outlib=lib_base;
   select class shoes cars;
run;

proc copy inlib=sashelp outlib=lib_comp;
   select class shoes cars stocks;
run;

data lib_comp.class;
   set lib_comp.class;
   if name="John" then do;
      height+1;
      weight+2;
   end;
run;

%compare(base=lib_base, comp=lib_comp)

libname lib_base;
libname lib_comp;

Compare the lib_base and lib_comp libraries, reporting on library
differences and comparing like named datasets;

======================================================================

* use the SPDE engine to create temporary libraries ;
libname lib_base spde "%sysfunc(pathname(work))" temp=yes;
libname lib_comp spde "%sysfunc(pathname(work))" temp=yes;

proc copy inlib=sashelp outlib=lib_base;
   select class shoes cars stocks;
run;

proc copy inlib=sashelp outlib=lib_comp;
   select class shoes cars;
run;

* filter the library using prxmatch syntax ;
%compare(base=lib_base, comp=lib_comp, filter=cla*|shoes)

libname lib_base;
libname lib_comp;

Compare the lib_base and lib_comp libraries, reporting on library
differences and comparing like named datasets, where the datasets
in the base library match the specified filter.

======================================================================

* use the SPDE engine to create temporary libraries ;
libname lib_base spde "%sysfunc(pathname(work))" temp=yes;
libname lib_comp spde "%sysfunc(pathname(work))" temp=yes;

proc copy inlib=sashelp outlib=lib_base;
   select class shoes;
run;

proc copy inlib=sashelp outlib=lib_comp;
   select class shoes;
run;

data lib_comp.class;
   set lib_comp.class;
   if name="John" then do;
      height+1;
      weight+2;
   end;
run;

* create a keys (BY processing) dataset ;
* the keys variable can have any name, but MUST be ;
* the 2nd variable in the PDV ;
data keys;
   length memname $32 keys $1024;
   infile datalines dsd dlm=":";
   input memname keys;
   datalines;
class : name sex
shoes : region product subsidiary stores
;
run;

* use the keys dataset to do a sorted comparison ;
%compare(base=lib_base, comp=lib_comp, by=keys)

* alternatively, specify the by variables on a per dataset basis ;
%compare(base=lib_base.class, comp=lib_comp.class, by=name sex)
%compare(base=lib_base.shoes, comp=lib_comp.shoes, by=region product subsidiary stores)

libname lib_base;
libname lib_comp;

----------------------------------------------------------------------

Notes:

If an allocated libref and dataset have the same name, the dataset
will take precedence, and a dataset comparison will be done.

A keys dataset can also be created using this proc contents approach:

* create a keys dataset for certain datasets ;
proc contents
   data=sashelp._all_
   out=contents (where=(memname in ("CLASS","SHOES")))
   noprint;
run;

proc sort;
   by libname memname varnum;
run;

data keys;
   format libname memname;
   length keys $1024 flag 8;
   retain keys;
   set contents;
   by libname memname;
   if first.memname then call missing(keys);
   flag=0;
   if prxmatch("/height|weight|age/io",strip(name)) then flag=1;  * variable exclusion ;
   if not prxmatch("/name|sex|region|product|subsidiary/io") then flag=1;  * variable inclusion ;
   if flag=0 then keys=catx(" ",keys,name);
   if last.memname then output;
   keep memname keys;
run;

--------------------------------------------------------------------*/

%macro compare
/*--------------------------------------------------------------------
PROC COMPARE either two datasets or two libraries
--------------------------------------------------------------------*/
(BASE=         /* Base dataset or library (REQ).                    */
,COMP=         /* Compare dataset or library (REQ).                 */
,BY=           /* By variables that uniquely identify a record (Opt)*/
               /* This can be either an explicit list of variables  */
               /* for a dataset, or a metadata dataset              */
               /* If specified, these variables should uniquely     */
               /* identify a record.  Otherwise, a warning message  */
               /* is issued, but the compare continues with the     */
               /* next BY group.                                    */
               /* This option is ignored when comparing libraries.  */
,FILTER=       /* Only process specified datasets? (Opt).           */
               /* This option is only used when comparing libraries.*/
               /* If specified, it must be a valid regular          */
               /* expression in prxmatch syntax.                    */
,CHECKOBS=0    /* Number of observations to print from the check    */
               /* dataset (REQ).                                    */
               /* Default is 0, no printing done.                   */
               /* Specify blank or "max" to print all observations. */
,MAXPRINT=%str(50,1000)
               /* Maximum PROC COMPARE differences to print (REQ).  */
,CRITERION=.000001
               /* Fuzz factor for numeric comparisons (REQ).        */
,METHOD=EXACT
               /* Specifies the method for judging the equality of  */
               /* numeric values                                    */
);

%local macro parmerr base_type comp_type type by_dataset by_var word obs;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(BASE,          _req=1,_words=0,_case=N)
%parmv(COMP,          _req=1,_words=0,_case=N)
%parmv(BY,            _req=0,_words=1,_case=N)
%parmv(FILTER,        _req=0,_words=1,_case=N)
%parmv(CHECKOBS,      _req=0,_words=0,_case=U)
%parmv(MAXPRINT,      _req=1,_words=1,_case=U)
%parmv(CRITERION,     _req=1,_words=0,_case=U)
%parmv(METHOD,        _req=1,_words=0,_case=U,_val=ABSOLUTE EXACT PERCENT RELATIVE)

%if (&parmerr) %then %goto quit;

%* is this a dataset or library compare? ;
%if (%sysfunc(exist(&base,data))  eq 1 or
     %sysfunc(exist(&base,view))) eq 1 %then %let base_type=DATA;
%else
%if (%sysfunc(libref(&base)) eq 0) %then %let base_type=LIB;
%else
%parmv(_msg=Either &base is not allocated or does not exist);

%if (%sysfunc(exist(&comp,data))  eq 1 or
     %sysfunc(exist(&comp,view))) eq 1 %then %let comp_type=DATA;
%else
%if (%sysfunc(libref(&comp)) eq 0) %then %let comp_type=LIB;
%else
%parmv(_msg=Either &comp is not allocated or does not exist);

%if (&parmerr) %then %goto quit;

%* both parameters must be the same type ;
%if (&base_type eq LIB  and &comp_type eq LIB)  %then %let type=LIB;
%else
%if (&base_type eq DATA and &comp_type eq DATA) %then %let type=DATA;
%else
%parmv(_msg=Both the BASE parameter (&base_type) and COMPARE parameter (&comp_type) must be the same type)

%if (&parmerr) %then %goto quit;

%* use SPDE work library for better performance ;
%if (%sysfunc(libref(workspde)) ne 0) %then %do;
   libname workspde spde "%sysfunc(pathname(work))" temp=yes;
%end;

%* capture current value of obs option ;
%let obs=%sysfunc(getoption(obs));

%* if a library comparison, create a comparison report ;
%* then recursively call this macro to compare like named datasets ;
%if (&type eq LIB) %then %do;
   %* temporarily set obs option to max ;
   %* SAS dataset options do not work with the dictionary tables ;
   options obs=max;

   proc sql noprint;
      create table workspde.compare_libraries as
         select
             case when base.memname is null then 0 else 1 end as in_base
            ,case when comp.memname is null then 0 else 1 end as in_comp
            ,case when sum(calculated in_base,calculated in_comp) ne 2 then
               "NO MATCH"
            else
               "MATCHED"
            end                                          as matched
            ,base.libname                                as base_libname
            ,base.memname                                as base_memname
            ,base.nlobs                                  as base_nobs      format=comma21.
            ,comp.libname                                as comp_libname
            ,comp.memname                                as comp_memname
            ,comp.nlobs                                  as comp_nobs      format=comma21.
         from (
            select
                libname
               ,memname
               ,nlobs
            from
               dictionary.tables
            where
               upcase(libname)="%upcase(&base)"
         ) base
         full outer join (
            select
                libname
               ,memname
               ,nlobs
            from
               dictionary.tables
            where
               upcase(libname)="%upcase(&comp)"
         ) comp
         on
            upcase(base.memname)=upcase(comp.memname)
         %if (%superq(filter) ne ) %then %do;
         where
            prxmatch("/&filter/io",base.memname)
         %end;
         order by
            calculated matched, base.memname
   ;
   %* create macro variable containing matching member names ;
   %global datasets;
   %let datasets=;
   select
      base_memname into :datasets separated by " "
   from
      workspde.compare_libraries
   where
      in_base and in_comp
   ;
   quit;

   options obs=&obs;

   title;
   title1 "Library comparison report between &base  and &comp  libraries";
   proc report data=workspde.compare_libraries (obs=max) nowd;
      column
      ("_Flags_"     in_base in_comp matched)
      ("_Base_"      base_libname base_memname base_nobs)
      ("_Compare_"   comp_libname comp_memname comp_nobs)
      obsdiff
      ;
      define         in_base        / display   spacing=0         "Base";
      define         in_comp        / display                     "Compare";
      define         matched        / display                     "Matched?";
      define         base_libname   / display                     "Libname";
      define         base_memname   / display                     "Memname";
      define         base_nobs      / display   format=comma21.   "# of Obs";
      define         comp_libname   / display                     "Libname";
      define         comp_memname   / display                     "Memname";
      define         comp_nobs      / display   format=comma21.   "# of Obs";
      define         obsdiff        / computed  width=9           "Obs Diff?";
      compute        obsdiff        / char      length=3;
         if comp_nobs ne base_nobs then obsdiff="<<<";
      endcomp;
  quit;

   %* now recursively call this macro over each matching dataset ;
   %if (&datasets eq ) %then %goto quit;

   %macro code;
      %compare(
          base=&base..&word
         ,comp=&comp..&word
         ,by=&by
         ,filter=&filter
         ,checkobs=&checkobs
         ,maxprint=%quote(&maxprint)
         ,criterion=&criterion
         ,method=&method
      )
   %mend;
   %loop(&datasets)
%end;
%else
%if (&type eq DATA) %then %do;
   %* save values of &base and &comp for titles statement ;
   %let base_title=&base;
   %let comp_title=∁

   %* if BY parameter was specified, was it a metadata dataset? ;
   %if (&by ne ) %then %do;
      %* save value of current BY parameter as possible BY_DATASET ;
      %let by_dataset=&by;
      %if (%sysfunc(exist(&by_dataset))) %then %do;
         %if (&by_var eq ) %then %do;
            %let dsid   = %sysfunc(open(&by_dataset));
            %let by_var = %sysfunc(varname(&dsid,2));  %* dataset must be memname keys format, so "keys" is 2nd variable ;
            %let dsid   = %sysfunc(close(&dsid));
         %end;
         %let by=;
         data _null_;
            set &by_dataset;
            where upcase(memname)="%upcase(%scan(&base,2,.))";
            call symputx("by",&by_var);
         run;
      %end;
      %else %do;
         %* not a BY_DATASET, assume it is a valid list of variables ;
         %let by_dataset=;
      %end;
   %end;

   %* have to sort the datasets, PROC COMPARE will not use indexes ;
   %if (&by ne ) %then %do;
      %kill(data=work._temp_)

      %* put the BY variables at the beginning of the PDV ;
      data work._temp_/view=work._temp_;
         format &by dummy;
         set &base (obs=&obs);
         retain dummy "";
      run;

      proc sort data=work._temp_ out=workspde._base_ %if %varexist(work._temp_,loaddttm) %then (drop=loaddttm);;
         by &by;
      run;

      %kill(data=work._temp_)

      %* put the BY variables at the beginning of the PDV ;
      data work._temp_/view=work._temp_;
         format &by dummy;
         set &comp (obs=&obs);
         retain dummy "";
      run;

      proc sort data=work._temp_ out=workspde._comp_ %if %varexist(work._temp_,loaddttm) %then (drop=loaddttm);;
         by &by;
      run;

      %kill(data=work._temp_)

      %let base=workspde._base_;
      %let comp=workspde._comp_;

      data workspde.check;
         format in;
         merge
            &base (in=base keep=&by)
            &comp (in=comp keep=&by)
         ;
         by &by;
         if base and comp then delete;
         if base then in="BASE record";
         if comp then in="COMP record";
      run;
   %end;
   %else %do;
      %* no BY variables, so assume both datasets are in desired sort order ;
      %* interleave datasets (merge with no by statement) ;
      data workspde.check;
         format in;
         merge
            &base (in=base keep=&by)
            &comp (in=comp keep=&by)
         ;
         if base and comp then delete;
         if base then in="BASE record";
         if comp then in="COMP record";
      run;
   %end;

   title1 "Comparison of &base_title  (base) to &comp_title  (compare) datasets";
   title3 "Records not in both datasets";
   data _null_;
      if (nobs) then do;
         put / "There are " nobs "records that are not shared between the &base and &comp datasets" /;
      end;
      stop;
      set workspde.check nobs=nobs;
   run;

   %if (&checkobs eq %str( ) or &checkobs eq MAX or &checkobs gt 0) %then %do;
      proc print data=workspde.check
      %if (&checkobs ne ) %then
         (obs=&checkobs);
      %else
         (obs=max);
      ;
         var in &by;
      run;
   %end;

   %* now compare the two datasets themselves ;
   title1 "Comparing &base_title  and &comp_title  datasets";
   proc compare
      base=&base
      comp=&comp
      listbasevar
      listcompvar
      warning
      maxprint=(&maxprint)
      method=&method
      %if (&method ne EXACT) %then %do;
      criterion=&criterion
      %end;
      ;
      %if (&by ne ) %then %do;
      id &by;
      %end;
   run;

   %let by=&by_dataset;
%end;

%quit:
%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : squote.sas
Purpose                 : Wrap the argument in single quotes
SAS Version             : SAS 9.3
Input Data              : Text or macro variable
Output Data             : Single quoted text or resolved macro variable

Macros Called           : None

Originally Written by   : Scott Bass
Date                    : 28OCT2016
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

Based on original work by SAS Communities user "Tom".
https://communities.sas.com/t5/SAS-Communities-Library/Not-All-Macro-Language-Elements-Are-Supported-by-the-Macro/ta-p/223904
Used with permission.

=======================================================================

Modification History    : Original version

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

options symbolgen;

%put %squote();

=======================================================================

%put %squote(foo);

=======================================================================

%let mvar=bar;
%put %squote(&mvar);

=======================================================================

%let mvar='blah';
%put %squote(&mvar);

=======================================================================

* unbalanced quote testing ;
* first get a simple %put to work ;
* you may have to restart SAS a lot during this testing ;

* these do not work ;
%let mvar=P.J. O'Briens;            %put &=mvar;
%let mvar=%str(P.J. O'Briens);      %put &=mvar;
%let mvar=%nrstr(P.J. O'Briens);    %put &=mvar;
%let mvar=%quote(P.J. O'Briens);    %put &=mvar;

%let mvar=P.J. O'Briens;            %put %bquote(&mvar);
%let mvar=%str(P.J. O'Briens);      %put %bquote(&mvar);
%let mvar=%nrstr(P.J. O'Briens);    %put %bquote(&mvar);
%let mvar=%quote(P.J. O'Briens);    %put %bquote(&mvar);

* this works ;
%let mvar=%bquote(P.J. O'Briens);   %put &=mvar;
%let mvar=%bquote(P.J. O'Briens);   %put %bquote(&mvar);

* so, for %put to work, the unbalanced quotes must be macro quoted ;
* in the call to %squote ;

* this works because the parameter is macro quoted, ;
* and the %squote macro returns balanced quotation marks ;
* so the %put statement works ;
%let mvar=%bquote(P.J. O'Briens);   %put %squote(&mvar);

* assigning the results of %squote to a macro variable also works ;
* these do not work ;
%let mvar=%str(P.J. O'Briens);
%let newmvar = %squote(%bquote(&mvar));
%put &=newmvar;

%let mvar=%str(P.J. O'Briens);
%let newmvar = %squote(%superq(mvar));
%put &=newmvar;

* but this does ;
%let mvar=%bquote(P.J. O'Briens);
%let newmvar = %squote(&mvar);
%put &=newmvar;

-----------------------------------------------------------------------
Notes:

Compare the functionality of this macro to SAS's %tslit macro.
Besides the hideous hard-to-remember name of SAS's macro,
this macro is "cleaner", especially when symbolgen is active.

---------------------------------------------------------------------*/

%macro squote
/*---------------------------------------------------------------------
Wrap the argument in single quotes.
---------------------------------------------------------------------*/
(VALUE         /* Value to wrap in single quotes (Opt).              */
);

%unquote(%str(%')%qsysfunc(tranwrd(%superq(value),%str(%'),''))%str(%'))
%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : varlist.sas
Purpose                 : Returns a string containing a space separated
                          list of variables in a dataset
SAS Version             : SAS 9.3
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 06DEC2007
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    :

Programmer              : Scott Bass
Date                    : 07JUL2017
Change/reason           : Added UPCASE parameter
Program Version #       : 1.1

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

%put %varlist(sashelp.shoes);

Outputs the variable list of the source dataset in PDV order.

Often this would be assigned to a macro variable, i.e.
%let varlist=%varlist(sashelp.class)

======================================================================

%put %varlist(sashelp.shoes,upcase=Y);

Same as above but uppercasing the returned variable list;

======================================================================

Error Processing:

%put %varlist(sashelp.doesnotexist);

%put %varlist(sashelp.shoes(keep=Region Subsidiary Sales Returns));
%put %varlist(sashelp.shoes(keep=Region--Returns));
%put %varlist(sashelp.shoes(keep=_character_));
%put %varlist(sashelp.shoes(keep=_numeric_));
%put %varlist(sashelp.shoes (drop=Region Subsidiary Sales Returns));
%put %varlist(sashelp.shoes (drop=Region--Returns));
%put %varlist(sashelp.shoes (drop=_character_));
%put %varlist(sashelp.shoes (drop=_numeric_));

-----------------------------------------------------------------------
Notes:

This macro is a "pure macro" and returns an rvalue.  It must be on the
right side of an assignment statement, or otherwise used where an
rvalue is appropriate, eg. %put %varlist(sashelp.class).

Note that dataset options (esp. KEEP= and DROP=) are not allowed.
This is due to a shortcoming in the VARNAME function, since it is
reading the variable number from the dataset header and not from a
logical PDV.  If you remove the error checking on dataset options
and run the above error checks, you will get warnings from the
varname function.

---------------------------------------------------------------------*/

%macro varlist
/*---------------------------------------------------------------------
Return a string containing a space separated list of variables in a
dataset.
---------------------------------------------------------------------*/
(DATA          /* Input dataset (REQ)                                */
,UPCASE=N      /* Uppercase the returned variable names? (Opt).     */
               /* Default value is NO.  Valid values are:           */
               /* 0 1 OFF N NO F FALSE and ON Y YES T TRUE          */
               /* OFF N NO F FALSE and ON Y YES T TRUE              */
               /* (case insensitive) are acceptable aliases for     */
               /* 0 and 1 respectively.                             */
);

%local macro parmerr varlist;
%let macro = &sysmacroname;

%* additional error checking ;
%if (%index(%superq(data),%str(%())) %then %do;
   %parmv(_msg=Dataset options are not allowed);
   %goto quit;
%end;

%* check input parameters ;
%parmv(DATA,         _req=1,_words=1,_case=N)
%parmv(UPCASE,       _req=0,_words=0,_case=U,_val=0 1)

%if (&parmerr) %then %goto quit;

%* open dataset for input ;
%let dsid=%sysfunc(open(&data,i));

%* if open failed, abort ;
%if (&dsid le 0) %then %do;
   %parmv(_msg=Unable to open &data);
   %goto quit;
%end;

%do i=1 %to %sysfunc(attrn(&dsid,nvars));
   %let varlist=&varlist %sysfunc(varname(&dsid,&i));
%end;
%let dsid=%sysfunc(close(&dsid));

%if (&upcase) %then %let varlist=%upcase(&varlist);
&varlist

%quit:
%mend;

/******* END OF FILE *******/


text:
/*====================================================================
Program Name            : execpath.sas
Purpose                 : Returns either the full path or the file name
                          of the currently executing program, in all of
                          DMS, EG, line mode, and batch mode,
                          on both Unix and Windows.
SAS Version             : SAS 9.1.3
Input Data              : N/A
Output Data             : N/A

Macros Called           : None

Originally Written by   : Scott Bass
Date                    : 09SEP2010
Program Version #       : 1.0

======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    : Original version

====================================================================*/

/*--------------------------------------------------------------------
Usage:

In DMS (say from Windows):

%put %execpath;
   Returns "DMS Process" if the Program Editor is unsaved
   Returns "Full program filename" if the Program Editor is saved,
      i.e. open on a saved file.

=======================================================================

In EG:

%put %execpath;
   Returns "EG Project Name:EG Task Label", for example:
   C:\Documents and Settings\sbass\My Documents\SAS\My SAS Projects\test1.egp:temp

=======================================================================

In line mode SAS (say from Unix):

sas -nodms

1?  %put %execpath;
   Returns "Line Mode Process"

=======================================================================

In batch mode SAS (either Windows or Unix):

sas c:\temp\test.sas, or
sas ~/mysasfiles/test.sas

Where test.sas contains %put %execpath;
   Returns "Full program filename"

%put %execpath(basename=Y);
   Returns the basename of the file, i.e. the filename only, not the
   full path.

----------------------------------------------------------------------
Notes:

This macro is a function style macro, so must be called as an rvalue
(right hand side of the equal sign) or in the context of a function
call.

--------------------------------------------------------------------*/

%macro execpath
/*--------------------------------------------------------------------
Returns the full path or filename of the currently executing program.
--------------------------------------------------------------------*/
(BASENAME=N    /* Return the filename only? (Opt).                  */
               /* Default is N.  If Y, then only the filename is    */
               /* returned.                                         */
);

%local macro parmerr;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(BASENAME,     _req=0,_words=0,_case=U,_val=0 1)

%if (&parmerr) %then %goto quit;

%let execpath=;

%* Batch mode ;
%let execpath=%sysfunc(getoption(SYSIN));

%* DMS (Windows only) ;
%* returns either Saved File ("File Name") or Untitled Editor ("DMS Process") ;
%if %length(&execpath)=0 %then
   %if (&sysscp eq WIN) %then
      %if (%index(&sysprocessname,DMS)) %then
         %let execpath=%sysget(SAS_EXECFILEPATH);

%* EG ;
%if (%symexist(_CLIENTAPP)) %then %do;
   %if (&_CLIENTAPP eq SAS Enterprise Guide) %then %do;
      %local project program;

      %* If submitting a saved program return the program path ;
      %* Else return the project path:task label ;
      %if (%symexist(_SASPROGRAMFILE)) %then %do;
         %let execpath=%scan(&_SASPROGRAMFILE,1,%str(%'%"));
      %end;

      %if (&execpath eq ) %then %do;
         %if (%symexist(_CLIENTPROJECTNAME)) %then
            %let project=&_CLIENTPROJECTNAME;
         %if (&project eq ) %then %let project=Unsaved EG Project;

         %if (%symexist(_EGTASKLABEL)) %then
            %let program=&_EGTASKLABEL;

         %let execpath=&project:&program;
      %end;
   %end;
%end;

%* All Other Scenarios ;
%if %length(&execpath)=0 %then
   %let execpath=&sysprocessname;

%if (not &basename) %then %do;
%unquote(&execpath)
%end;
%else %do;
%unquote(%scan(&execpath,-1,%str(/\:)));
%end;

%quit:
%* if (&parmerr) %then %abort;

%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : realloc_concat_libs.sas
Purpose                 : Reallocate concatenated libraries so that all
                          levels > 1 are allocated as readonly
SAS Version             : SAS 9.3
Input Data              : N/A
Output Data             : N/A

Macros Called           : None

Originally Written by   : Scott Bass
Date                    : 23MAY2013
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    : Original version

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

-----------------------------------------------------------------------
Notes:

Usually this macro is called as an initstmt for the Lev2 and Lev9
environments.  It must be an initstmt because the autoexec executes
before the metadata library allocations.  This macro must be called
after the metadata library allocations are completed.

---------------------------------------------------------------------*/

%macro realloc_concat_libs
/*---------------------------------------------------------------------
Reallocate concatenated libraries so that all levels > 1 are allocated
as readonly.
---------------------------------------------------------------------*/
;

%let options=%sysfunc(getoption(notes)) %sysfunc(getoption(source)) %sysfunc(getoption(nosource));
options nonotes nosource nosource2;
proc sql noprint;
   create table work._concat_libs_ as
   select
      libname, path, engine, level
   from
      dictionary.libnames
   where
      level > 0 and libname ne "SASHELP"
   ;
run;

filename temp temp;

data _null_;
   set work._concat_libs_;
   file temp;
   by libname notsorted;
   if (level eq 1) then
      put "libname tmp" level '"' path +(-1) '";';
   else
      put "libname tmp" level '"' path +(-1) '" access=readonly;';
   if last.libname then do;
      put "libname " libname engine "(" @;
      do i=1 to level;
         put "tmp" i @;
      end;
      put +(-1) ");";
      do i=1 to level;
         put "libname tmp" i "clear;";
      end;
      put;
   end;
run;

proc delete data=work._concat_libs_;
run;

%include temp / nosource nosource2;
filename temp;
options &options;

%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : pagexofy.sas
Purpose                 : Add page numbers to output, usually in the
                          form of "Page x of Y".
SAS Version             : SAS 9.1.3
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 31AUG2010
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    : Original version

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

title;
footnote;

%macro create_sample_output;
   * first generate some sample output ;
   filename old temp;  * best practice - use a temporary file for initial file ;
   filename new temp;

   * alternative files, useful for debugging by viewing in an editor ;
   filename old "c:\temp\old.lst";
   filename new "c:\temp\new.lst";

   * send your desired output to the working file ;
   proc printto print=old new;
   run;

   options pageno=1;
   proc print data=sashelp.shoes (obs=40) noobs uniform;
      var region--inventory;
   run;

   * release the output file ;
   proc printto;
   run;
%mend;

options mprint;
options ps=20 ls=80;
options nocenter nodate number;

-----------------------------------------------------------------------

title "Testing the pagexofy macro";
%create_sample_output;

* now post-process the file to generate "Page x of Y" output ;
* you can overwrite the source file ;
%pagexofy(infile=old);

=======================================================================

* or you can create a new file ;
%create_sample_output;
%pagexofy(infile=old,outfile=new);
%pagexofy(infile="c:\temp\old.lst",outfile=c:\temp\new.lst);

=======================================================================

* this is the default style ;
* you must use {PAGE} and {NUMPAGES} (case-insensitive) ;
* as tokens for <current> and <total> pages ;
%pagexofy(infile=old,outfile=new,style=Page {PAGE} of {NUMPAGES});

=======================================================================

* you can change the style ;
%pagexofy(infile=old,outfile=new,style=PAGE {PAGE} -of- {numpages});
%pagexofy(infile=old,outfile=new,style=Seite {Page} von {NumPages});
%pagexofy(infile=old,outfile=new,style=(Page {PAGE} of {NUMPAGES}));
%pagexofy(infile=old,outfile=new,style=[Page {PAGE} of {NUMPAGES}]);
%pagexofy(infile=old,outfile=new,style={Page <{PAGE}> of <{NUMPAGES}>});

=======================================================================

* or you can just embellish the current page number ;
%pagexofy(infile=old,outfile=new,style=PAGE >>>{PAGE}<<<);

=======================================================================

* specify the JUSTIFY parameter to justify the page renumbering ;
options nonumber;
title "Testing the pagexofy macro";
%create_sample_output;

* note the undesired output for left justification ;
* due to the title statement ;
%pagexofy(infile=old,outfile=new,justify=right);  * default ;
%pagexofy(infile=old,outfile=new,justify=left);
%pagexofy(infile=old,outfile=new,justify=center);

=======================================================================

* or you can use an explicit column pointer for total control ;
* this sets the beginning column position, i.e. the start of "Page..." ;
%pagexofy(infile=old,outfile=new,justify=50);

=======================================================================

* you can explicitly set the linesize ;
* useful if no titles were used or nonumber option was set ;
* so that the effective linesize cannot be reliably determined ;
options nonumber nodate;
title " ";  * you want this, print a blank title ;
%create_sample_output;
%pagexofy(infile=old,outfile=new,justify=right,ls=90);

All of the above forms of the macro call use the form feed character ("0C"x)
to count the number of pages.

=======================================================================

You can also use a specific delimiter as a token to count the number
of pages.  This delimiter would typically be in a title or footnote statement.

* Generate new sample output ;

title "Testing the pagexofy macro with a delimiter __PAGE__";
%create_sample_output;
%pagexofy(infile=old,outfile=new,delim=__PAGE__);

=======================================================================

* DELIM in conjunction with creative use of titles statements can be useful ;

options center nonumber nodate;
title1 "Acme Pharmaceutical Company";
title2 "%sysfunc(date(),date9.)";
title3 "__PAGE__";
%create_sample_output;
%pagexofy(infile=old,outfile=new,delim=__PAGE__,justify=CENTER,ls=80);

=======================================================================

* Support for user-defined delimiter allows placing the page renumbering ;
* at the bottom of the page via the footnote statement ;

options center nonumber nodate;
title1 "Acme Pharmaceutical Company";
title2 "%sysfunc(date(),date9.)";
footnote1 "Prepared by:  &sysuserid on &sysday &sysdate9 __PAGE__";
%create_sample_output;
%pagexofy(infile=old,outfile=new,delim=__PAGE__,justify=RIGHT);

=======================================================================

options nocenter nonumber nodate;
title1 "Acme Pharmaceutical Company";
title2 "%sysfunc(date(),date9.)";
footnote1 "Prepared by:  &sysuserid on &sysday &sysdate9";
footnote2 "__PAGE__";
%create_sample_output;
%pagexofy(infile=old,outfile=new,delim=__PAGE__,justify=LEFT);

-----------------------------------------------------------------------
Notes:

Either a file reference or a physical path can be used for the INFILE
and OUTFILE parameters.  If a file reference is used it must be
allocated outside this macro.  If a physical path is used, it must 1)
be quoted with single or double quotes, 2) contain a slash (forward or
backward), period, or colon, or 3) be longer than 8 characters.
Otherwise it will be treated as a file reference.

If the OUTFILE parameter is blank, the INFILE will be overwritten
with the re-paginated output.

Updating a file in place using the SAS sharebuffers infile statement
option is problematic.  Therefore, when updating a file in place, an
intermediate temporary file is used as a buffer, and this macro makes a
recursive call to itself.  This approach has a slight performance hit,
since it involves two passes over the output file, but in most cases
this should not be an issue unless the initial file is enormous.

However, best practice is to use a temporary file in the calling code
for the initial output (usually created via proc printto or ods listing
statement).  By reading and writing to separate files, the above
performance hit is avoided.

The STYLE field codes {PAGE} and {NUMPAGES} are "borrowed" from the
equivalent field codes in Microsoft Word.

The DELIM parameter should be unique to the generated output, i.e.
should occur once and only once per page.  Typically it is specified in
a title or footnote statement.

The DELIM parameter must be 20 characters or less.

Normally the longest line in the output is used to determine the
effective linesize.  This works fine if you have the number option set
when the output is generated, since the page number will be right
justified within the pagesize in effect when the output was generated,
and then overwritten when the output is re-paginated.  However, if no
titles were used, or the nonumber option was in effect, you can use the
LS parameter to explicitly set the linesize used to justify the page
renumbering.

By default the "Page x of Y" output is right justified using this
derived linesize.  Use the JUSTIFY parameter to override the default
justification.

If you specify LEFT or CENTER justification, you'll likely want to turn
off titles, or use a user-specified delimiter, or you'll probably get
unwanted results (the page renumbering overlaying other title text).

---------------------------------------------------------------------*/

%macro pagexofy
/*---------------------------------------------------------------------
Add page numbers to output, usually in the form of "Page x of Y"
---------------------------------------------------------------------*/
(INFILE=       /* Input file (REQ).                                  */
,OUTFILE=      /* Output file (Opt).                                 */
               /* If blank, the input file is overwritten.           */
,STYLE=Page {PAGE} of {NUMPAGES}
               /* Style to use for the pagination (REQ).             */
               /* Usually of the form "Page x of Y" or "Page x"      */
               /* "{PAGE}" represents the current page number.       */
               /* "{NUMPAGES}" represents the total pages in the     */
               /* file, as determined by counting either the         */
               /* form feeds or a user-specified delimiter.          */
,DELIM=\f      /* Delimiter used to count pages (REQ).               */
               /* By default the form feed character is used.        */
,JUSTIFY=RIGHT /* Justification of page renumbering (REQ).           */
               /* Default is RIGHT.                                  */
               /* Valid values are LEFT L CENTER C RIGHT R, or a     */
               /* specific integer value to hard code the beginning  */
               /* column position.                                   */
,LS=           /* Explicit line size (Opt).  If blank the linesize   */
               /* is derived from the longest line in the source file*/
);

%local macro parmerr;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(INFILE,       _req=1,_words=1,_case=N)
%parmv(OUTFILE,      _req=0,_words=1,_case=N)
%parmv(STYLE,        _req=1,_words=1,_case=N)
%parmv(DELIM,        _req=1,_words=0,_case=N)
%parmv(JUSTIFY,      _req=1,_words=0,_case=U)
%parmv(LS,           _req=0,_words=0,_val=POSITIVE)

%if (&parmerr) %then %goto quit;

%* additional error checking ;
%* further checks on the JUSTIFY parameter ;
%if (&justify eq LEFT)   %then %let justify=L;
%else
%if (&justify eq CENTER) %then %let justify=C;
%else
%if (&justify eq RIGHT)  %then %let justify=R;

%let _error=1;
%do i=1 %to 3;
   %let word=%scan(L C R,&i);
   %if (&justify eq &word) %then %let _error=0;
%end;

%if (&_error) %then %do;
   %if %sysfunc(verify(&justify,0123456789)) %then %let _error = 1;
   %else
   %if ^(&justify) %then %let _error = 1;
   %else %let _error = 0;
%end;

%if (&_error) %then %do;
   %parmv(_msg=Invalid value &justify for JUSTIFY.  Valid values are LEFT L CENTER C RIGHT R, or a positive integer);
   %goto quit;
%end;

%* was a physical file or a fileref specified? ;
%if (%sysfunc(findc(%superq(infile),%str(:./\%'%")))) or
    (%length(%superq(infile)) gt 8) %then
   %let infileref=0;
%else
   %let infileref=1;

%if (%sysfunc(findc(%superq(outfile),%str(:./\%'%")))) or
    (%length(%superq(outfile)) gt 8) %then
   %let outfileref=0;
%else
   %let outfileref=1;

%* does the input file exist? ;
%if (&infileref) %then
   %let exists=%sysfunc(fexist(%superq(infile)));
%else
   %let exists=%sysfunc(fileexist(%superq(infile)));

%if (not &exists) %then %do;
   %parmv(_msg=File %superq(infile) does not exist or is not allocated);
   %goto quit;
%end;

%* need to dequote INFILE and OUTFILE for later processing ;
%let infile =%scan(&infile, 1,%str(%"%'));
%let outfile=%scan(&outfile,1,%str(%"%'));

%* If OUTFILE is blank then the INFILE is overwritten (updated "in place") ;
%let inplace=0;
%if (%superq(OUTFILE) eq ) %then %do;
   %let outfile=&infile;
   %let inplace=1;
%end;

%* Updating a file in place is problematic using the infile SHAREBUFFERS option. ;
%* If updating in place, recursively call this macro, using a temporary file as a buffer ;
%if (&inplace) %then %do;
   filename __temp__ temp;
   %pagexofy(infile=&infile,outfile=__temp__);
   data _null_;
      infile __temp__;
      file &outfile;
      input;
      put _infile_;
   run;
   filename __temp__ clear;  /* this automatically deletes the temporary file */

   %goto quit;
%end;

%* SAS does not intrinsically know "a priori" how many pages of output it has created ;
%* Therefore, the output must be post-processed, after creation, in order to determine ;
%* 1) the number of pages created, and 2) re-paginating the output. ;
%* This requires two passes over the data (i.e. the previously created output). ;

%* First pass:  Derive the total page count and effective linesize ;
data pagecnt;
   length pattern $20 infile $256;
   pattern     = "&delim";
   prx         = prxparse(cats("/",pattern,"/"));
   pagecnt     = 1;

   %if (&infileref) %then %do;
   infile      = pathname("&infile");
   %end;
   %else %do;
   infile      = "&infile";
   %end;

   infile infile filevar=infile length=l end=eof;
   do until (eof);
      input;
      found = prxmatch(prx,_infile_);
      if found then numpages+1;
      ls=max(ls,l);
   end;

   /* if the delimiter is the form feed, need to account for the first page */
   numpages=numpages+(pattern eq "\f");

   /* if LS was specified, override the derivation of linesize */
   %if (&ls ne ) %then %do;
      ls=&ls;
   %end;
   keep numpages ls;
run;

%* Second pass:  Re-paginate output ;
data _null_;
   length pattern $20 infile outfile line $256;
   pattern     = "&delim";
   prx         = prxparse(cats("/",pattern,"/"));
   justify     = "&justify";

   %if (&infileref) %then %do;
   infile      = pathname("&infile");
   %end;
   %else %do;
   infile      = "&infile";
   %end;

   %if (&outfileref) %then %do;
   outfile     = pathname("&outfile");
   %end;
   %else %do;
   outfile     = "&outfile";
   %end;

   set pagecnt;
   infile infile filevar=infile end=eof /* sharebuffers */;
   %if (%superq(outfile) eq %superq(infile)) %then %do;
   file infile;
   %end;
   %else %do;
   file outfile filevar=outfile;
   %end;

   do _n_ = 1 by 1 until (eof);
      input;
      /* need a buffer, do not substr() _infile_ directly
         or you may get argument out of range errors */
      line=_infile_;
      found = prxmatch(prx,_infile_);
      if (_n_ eq 1 and pattern eq "\f") or found then do;  /* no form feed on first page so check _n_=1 */
         page + 1;
         style = "&style";
         style = prxchange(cats("s/{PAGE}/",    page,    "/i"),-1,style);
         style = prxchange(cats("s/{NUMPAGES}/",numpages,"/i"),-1,style);
         len=length(style);

         /* if a user-defined delimiter was specified blank it out */
         if (pattern ne "\f") then
            substr(line,found,length(pattern))=repeat(" ",length(pattern)-1);

         /* derive the column pointer based on JUSTIFY parameter */

         /* need to check if first character is a form feed and, if so,
            then offset the column pointer by 1 so we do not delete (overwrite) the form feed */
         ff=prxmatch("/\f/",substr(_infile_,1,1));
         select (justify);
            when ("R")
               do;
                  col=(ls-len)+ff;
                  substr(line,col,len)=style;
               end;
            when ("L")
               do;
                  col=1+ff;
                  substr(line,col,len)=style;
               end;
            when ("C")
               do;
                  col=int((ls - len)/2)+ff;
                  substr(line,col,len)=style;
               end;
            otherwise
               do;  /* must be an explicit column number */
                  col=input(justify,8.);
                  substr(line,col,len)=style;
               end;
         end;
      end;
      varlen=lengthn(line);
      put line $varying. varlen;
   end;
   stop;
run;

%quit:

%mend;

/******* END OF FILE *******/


text:
/*====================================================================
Program Name            : dedup_mstring.sas
Purpose                 : Removes duplicate words from a
                          macro variable string
SAS Version             : SAS 9.4
Input Data              : Macro variable string
Output Data             : r-value string with duplicates removed

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 02FEB2016
Program Version #       : 1.0

======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    : Original version

====================================================================*/

/*--------------------------------------------------------------------
Usage:

%let oldstring=C A B B A G E 3 2 1 1 2 3;
%put %dedup_mstring(&oldstring);

Dedups the input string oldstring, returning "C A B G E 3 2 1".
The default delimiter for a word is a space.

======================================================================

%let oldstring=C  A  B  B  A  G  E  3 2 1 1 2 3;
%let newstring=%dedup_mstring(&oldstring);
%put &=newstring;

Same as above, but assigns the result to &newstring.
Multiple input delimiters are replaced with a single output delimiter.

======================================================================

%let oldstring=%str(C, A, B, B, A, G, E, 3, 2, 1, 1, 2, 3);

* these examples do not work ;
%put %dedup_mstring(&oldstring,indlm=,);
%put %dedup_mstring(%quote(&oldstring),indlm=,);

* these examples work ;
%put %dedup_mstring(%quote(&oldstring),indlm=%str(,));
%put %dedup_mstring(%bquote(&oldstring),indlm=%str(,));
%put %dedup_mstring(%nrbquote(&oldstring),indlm=%str(,));
%put %dedup_mstring(%superq(oldstring),indlm=%str(,));

* this is what you need to output a space as the output delimiter ;
%put %dedup_mstring(%superq(oldstring),indlm=%str(,),dlm=);            * uses the input delimiter ;
%put %dedup_mstring(%superq(oldstring),indlm=%str(,),dlm=%str());      * uses the input delimiter ;
%put %dedup_mstring(%superq(oldstring),indlm=%str(,),dlm=%str( ));     * space ;
%put %dedup_mstring(%superq(oldstring),indlm=%str(,),dlm=%str(  ));    * two spaces ;
%put %dedup_mstring(%superq(oldstring),indlm=%str(,),dlm=%str( | ));   * space | space ;

Commas are common in macro variable strings, but are problematic
for the macro tokenizer.  Macro quoting functions must be used on the
macro invocation to get the desired results.

======================================================================

%let oldstring=C^A^B^B^A#G#E#3|2|1*1*2*3;

* output delimter is a space ;
%put %dedup_mstring(&oldstring,indlm=^#|*);

* output delimter is a comma ;
%let newstring=%dedup_mstring(&oldstring,indlm=^#|*,dlm=%str(,));
%put &=newstring;

Multiple delimiters may be specified for the input string to delimit
words, but only one delimiter can be specified for the output string.
(This is a rare use case.)

======================================================================

%let oldstring='PERSON', "ORGANISATION",AND,OR,NOT, 'PERSON', 'ORGANISATION';
%let newstring=%dedup_mstring(%quote(&oldstring),indlm=%str(,));
%put &=newstring;

Example of a complex string, including macro keywords.

For quoted strings, the quoting must be matched (start and end quotes)
and the same type (single or double quotes).

For the purposes of this macro, all characters other than the delimiter
are significant and used to determine whether a duplicate occurs

----------------------------------------------------------------------
Notes:

This macro returns an r-value, so the results are usually assigned to
another macro variable or else used inline.  The returned r-value is
macro unquoted.

This macro is a "pure macro" implementation and runs to completion
during program compilation.

An input delimiter is required to indicate what determines a "word".

The default input and output delimter is a space.

If the output delimiter is not specified:
   If the length of the input delimiter = 1, the input delimiter
      will be used for the output string.
   If the length of the input delimiter > 1, a space
      will be used for the output string.

If the output delimiter is specified, it will always be used.

--------------------------------------------------------------------*/

%macro dedup_mstring
/*--------------------------------------------------------------------
Removes duplicate words from a macro variable string
--------------------------------------------------------------------*/
(IN            /* Input string (REQ).                               */
,INDLM=        /* Input delimiter marking each token (word) in the  */
               /* input string (Opt).                               */
               /* If not specified, a space will be used.           */
,DLM=          /* Output delimiter marking each token (word) in the */
               /* output string (Opt).                              */
               /* If not specified:                                 */
               /*    If the length of INDLM = 1, set to INDLM       */
               /*    If the length of INDLM > 1, set to space       */
);

%local macro parmerr i out;
%let macro = &sysmacroname;
%let out   = ;

%* check input parameters ;
%parmv(IN,           _req=1,_words=1,_case=N)

%if (&parmerr) %then %goto quit;

%* if the input INDLM was not specified set it to a space ;
%if (%length(%superq(indlm)) eq 0) %then %let indlm=%str( );

%* if the output DLM was not specified: ;
%*    if the length of INDLM = 1, set DLM = INDLM ;
%*    if the length of INDLM > 1, set DLM to a space. ;
%if (%length(%superq(dlm)) eq 0) %then %do;
   %if (%length(%superq(indlm)) eq 1) %then
      %let dlm = %superq(indlm);
   %else
   %if (%length(%superq(indlm)) gt 1) %then
      %let dlm = %str( );
%end;

%* loop over each token, searching the target for that token ;
%let num=%sysfunc(countc(%superq(in),%str(&indlm)));
%let num=%eval(&num+1);
%do i=1 %to #
   %let word=%scan(%superq(in),&i,%str(&indlm));
   %let pos=%sysfunc(indexw(&out,&word,%str(&dlm)));
   %if (&pos eq 0) %then %do;
      %if (&i gt 1) %then %let out=&out%str(&dlm);
      %let out=&out&word;
   %end;
%end;
%unquote(&out)

%quit:

%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : max_decimals.sas
Purpose                 : Derives the maximum number of decimal places
                          within a given variable(s), either for the
                          entire dataset or by groups.
SAS Version             : SAS 9.1.3
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv, seplist

Originally Written by   : Scott Bass
Date                    : 01OCT2010
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    : Original version

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

* create test dataset ;
data test;
   length key $1 num1 num2 8 chr1 chr2 $9;
   input key num1 num2 chr1 chr2;
   datalines;
A  1              1           1           1
A  2.22           2.22        2.2         2.2
A  33.333         33.333      33.3        33.3
A  4.4444         4.444       4.44        BAD
B  555.55         555.5       555.55      555
B  66.6           66.6        66.66       66
B  7777.77        7777.7      7777.77     7777
C  8888.888       8888.888    8888.88     BAD
C  9999.9999      9999.99     9999.9      9999.9
;
run;

* various test invocations ;

* all numeric variables, only _maxdec_ dataset created ;
%max_decimals(data=test,out=_null_);

* all numeric variables, max decimals merged into new dataset ;
%max_decimals(data=test,out=test1);

* all character variables (should contain numeric data) ;
%max_decimals(data=test,out=test2,var=_character_);

* all variables ;
%max_decimals(data=test,out=test3,var=_all_);

* specify explicit variables ;
%max_decimals(data=test,out=test4,var=num1 chr2);

* specify explicit prefix ;
%max_decimals(data=test,out=test5,var=num2 chr1 chr2,prefix=my_max_);

* by key variables ;
* all numeric variables, max decimals merged into new dataset ;
%max_decimals(data=test,out=test6,by=key);
%max_decimals(data=test,out=test6,by=key,var=chr1 chr2);

* max decimals merged back into source dataset ;
%max_decimals(data=test);

* all numeric variables, max decimals merged into new data step view ;
* all the above examples can output a data step view via the type= parameter ;
%max_decimals(data=test,out=v_test,type=view);

-----------------------------------------------------------------------
Notes:

An output data step view, rather than dataset, can be useful
if the input dataset is very large.

If TYPE=view, then OUT= must be specified (or _null_).

If OUT=_null_, then TYPE= is irrelevant.

---------------------------------------------------------------------*/

%macro max_decimals
/*---------------------------------------------------------------------
Derives the maximum number of decimal places within a given variable(s)
---------------------------------------------------------------------*/
(DATA=         /* Input dataset. (REQ).                              */
,OUT=          /* Output dataset / view (Opt).                       */
               /* If blank, the derived maximum decimal variable(s)  */
               /* are merged back into the input dataset.            */
               /* If "_null_" (case-insensitive), only the _MAXDEC_  */
               /* working dataset is created, for use in the         */
               /* calling program.                                   */
,TYPE=DATA     /* Output dataset type (Opt).                         */
               /* Default is DATA.  If blank, then DATA is used.     */
               /* Valid values are DATA or VIEW.                     */
,VAR=_NUMERIC_ /* Input variables (REQ).                             */
               /* Default is all numeric variables in the input      */
               /* dataset.                                           */
,BY=           /* By variables (Opt).                                */
               /* If blank, the maximum decimal variable(s) are      */
               /* derived for the entire column.                     */
               /* If not blank, the maximum decimal variable(s) are  */
               /* derived for each by group.                         */
,PREFIX=maxdec_
               /* Output variable prefix (REQ).                      */
               /* Default is "maxdec_".  Output variables are named  */
               /* as &PREFIX.&VAR1, &PREFIX.&VAR2, etc.              */
);

%local macro parmerr;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(DATA,         _req=1,_words=0,_case=U)
%parmv(OUT,          _req=0,_words=0,_case=U)
%parmv(TYPE,         _req=0,_words=0,_case=U,_val=DATA VIEW)
%parmv(VAR,          _req=1,_words=1,_case=U)
%parmv(BY,           _req=0,_words=1,_case=U)
%parmv(PREFIX,       _req=1,_words=0,_case=N)

%if (&parmerr) %then %goto quit;

%* if out was not specified, merge results back into source dataset ;
%if (&out eq ) %then %let out=&data;

%* if type is blank then set to DATA ;
%if (&type eq ) %then %let type=DATA;

%* if type=VIEW then out must be different than data ;
%if (&type eq VIEW) %then
   %if (&out eq &data) %then %do;
      %parmv(_msg=If TYPE=VIEW then the OUT parameter must be diffent from the DATA parameter)
      %goto quit;
   %end;

%* get contents of source dataset ;
proc contents data=&data (keep=&var) out=_columns_ (keep=name type varnum) noprint;
run;

%* get list of numeric and character variables ;
%local num_vars chr_vars;
%let num_vars=;
%let chr_vars=;
proc sql noprint;
   select name into :num_vars separated by " " from _columns_ where type=1 order by varnum;
   select name into :chr_vars separated by " " from _columns_ where type=2 order by varnum;
   drop table _columns_;
quit;

%let max_dec_vars=%seplist(&num_vars &chr_vars,prefix=&prefix,dlm=%str( ));

%* create dataset deriving maximum number of decimal places per variable ;
data _maxdec_;
   set &data end=eof;

   %if (&by ne ) %then %do;
   by &by;
   %end;

   %if (&num_vars ne ) %then %do;array _num_ {*} &num_vars;%end;
   %if (&chr_vars ne ) %then %do;array _chr_ {*} &chr_vars;%end;

   array _maxdec_ {*} &max_dec_vars;
   retain _maxdec_;
   length _temp_ _int_ _dec_ $32;

   _i_=0;
   %if (&num_vars ne ) %then %do;
   do _i_=1 to dim(_num_);
      _temp_=put(_num_{_i_},best32.-L);
      _int_=scan(_temp_,1,".");
      _dec_=scan(_temp_,2,".");
      _maxdec_{_i_}=max(_maxdec_{_i_},lengthn(_dec_));
   end;
   _i_=_i_-1;
   %end;

   %if (&chr_vars ne ) %then %do;
   do _j_=1 to dim(_chr_);
      _temp_=put(input(_chr_{_j_},?? best32.),best32.-L);
      _int_=scan(_temp_,1,".");
      _dec_=scan(_temp_,2,".");
      _maxdec_{_j_+_i_}=max(_maxdec_{_j_+_i_},lengthn(_dec_));
   end;
   %end;

   %if (&by ne ) %then %do;
   if last.%scan(&by,-1,%str( )) then do;
      output;
      call missing(of &max_dec_vars);
   end;
   %end;
   %else %do;
   if eof then output;
   %end;

   keep &by &max_dec_vars;
run;

%* merge max decimal data back into original dataset ;
%if (%lowcase(&out) ne _null_) %then %do;
   %if (&by eq ) %then %do;
      data &out %if (&type eq VIEW) %then / view=&out;;
         set &data;
         if _n_=1 then set _maxdec_;
      run;
   %end;
   %else %do;
      data &out %if (&type eq VIEW) %then / view=&out;;
         merge &data _maxdec_;
         by &by;
      run;
   %end;
%end;

%quit:

%mend;

/******* END OF FILE *******/


text:
/*=====================================================================
Program Name            : TEMPLATE.sas
Purpose                 : PURPOSE OF THE MACRO
SAS Version             : SAS VERSION WHEN THE MACRO WAS ORIGINALLY WRITTEN
Input Data              : N/A
Output Data             : N/A

Macros Called           : LIST ANY UTILITY MACROS CALLED BY THIS MACRO
                          SO THEY CAN BE ADDED TO THE SASAUTOS LIBRARY AS WELL

Originally Written by   : YOUR FULL NAME (NOT INITIALS - MAKE IT EASY TO FIND YOU LATER)
Date                    : TODAYs DATE IN DDMONYYYY FORMAT (INTERNATIONLALLY UNAMBIGUOUS DATE FORMAT)
Program Version #       : 1.0

=======================================================================

Scott Bass (sas_l_739@yahoo.com.au)

This code is licensed under the Unlicense license.
For more information, please refer to http://unlicense.org/UNLICENSE.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.

=======================================================================

Modification History    : Original version

Programmer              : YOUR FULL NAME
Date                    : DDMONYYYY
Change/reason           : SUMMARY OF CODE CHANGE.
                          ADD COMMENTS IN THE CODE ITSELF
                          (INCLUDING NAME AND DATE) FOR ADDITIONAL DETAILS)
Program Version #       : 1.1

Programmer              : YOUR FULL NAME
Date                    : DDMONYYYY
Change/reason           : SUMMARY OF CODE CHANGE.
                          ADD COMMENTS IN THE CODE ITSELF
                          (INCLUDING NAME AND DATE) FOR ADDITIONAL DETAILS)
Program Version #       : 1.2

VERSION 1.0 (ORIGINAL VERSION):
   LEAVE "Original version" TEXT IN PLACE
   DELETE REVISION HISTORY

VERSION 1.1+ (MODIFIED VERSION):
   DELETE THE "Original version" TEXT
   ADD THE REVISION HISTORY PER ABOVE TEMPLATE

=====================================================================*/

/*---------------------------------------------------------------------
Usage:

LIST MACRO USE CASE / UNIT TESTS HERE
TRY TO USE UNIVERSALLY AVAILABLE SOURCE DATA IF POSSIBLE
(EG. SASHELP DATASETS, C:\WINDOWS, /USR/LOCAL, ETC.)
OR ELSE CREATE SIMPLE TEST DATA INLINE
SO THE END USER CAN RUN THE USE CASE WITHOUT FURTHER SETUP OR EFFORT.
IDEALLY THE TEST CASES RUN BY HIGHLIGHTING THE TEXT IN THE SAS EDITOR
AND HITTING F3

=======================================================================

ADDITIONAL USE CASE

=======================================================================

ADDITIONAL USE CASE

-----------------------------------------------------------------------
Notes:

EXPLANATORY NOTES HERE, THAT EXPLAIN DESIGN DECISIONS OR
GENERAL USAGE INFORMATION.

THESE ARE NOTES THAT GO BEYOND COMMENTS WITHIN THE MACRO CODE.

---------------------------------------------------------------------*/

%macro TEMPLATE
/*---------------------------------------------------------------------
PURPOSE OF THE MACRO (REDUNDANT BUT NICE TO HAVE HERE AS WELL)
---------------------------------------------------------------------*/
(PARM1         /* PARM1 EXPLANATION (REQ).                           */
,PARM2=default /* PARM2 EXPLANATION (Opt).  Default is "default"     */
);

%local macro parmerr _data_;
%let macro = &sysmacroname;

%* check input parameters ;
%parmv(PARM1,        _req=1,_words=1,_case=N)  /* words allows ds options */
%parmv(PARM2,        _req=0,_words=0,_case=N)

%if (&parmerr) %then %goto quit;

%* additional error checking ;
%if (&parm1 eq FOO and %superq(parm2) eq BAR) %then %do;
   %parmv(_msg=These two values are mutually exclusive)
   %goto %quit;
%end;

%* well commented macro code goes here ;

%* Use %* or /* */ comment style for comments that do not show in MPRINT output ;
%* Use * comment style for comments that show in MPRINT output ;

%quit:

%mend;

/******* END OF FILE *******/