text:
/*====================================================================
Program Name            : hash_lookup.sas
Purpose                 : Lookup satellite variables from a hash object.
SAS Version             : SAS 9.1.3
Input Data              : SAS input dataset loaded by %hash_define
Output Data             : None

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 12MAY2010
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                    : 20JAN2012
Change/reason           : Commented out error checking on hashnum parameter
Program Version #       : 1.1

Programmer              : Scott Bass
Date                    : 04FEB2016
Change/reason           : Added hashname option.
Program Version #       : 1.3

+===================================================================*/

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

See Usage in %hash_define macro header.

However, a multidata lookup deserves a special example.
Note that this example illustrates the *concepts* of the macro code,
not the macro code itself.

* Multidata lookup - better source/lookup example datasets ;
data lookup;
  set
    sashelp.class
    sashelp.class
    sashelp.class
  ;
run;

proc sort;
  by name;
run;

* build dummy date ranges ;
data lookup;
  set lookup;
  date=(_n_-1)*3;
  format date date7.;
run;

* it is useful walking this through the debugger ;
* remove "/ debug" if you do not want the debugger ;
data joined / debug;
  * set PDV variable attributes ;
  if 0 then set lookup;

  * create hash object ;
  if _n_=1 then do;
    declare hash _h1 (dataset: "lookup", multidata: "Y");
    _h1.defineKey("name");
    _h1.defineData(ALL: "Y");
    _h1.defineDone();
  end;
  call missing(of _all_);

  * the lookup key is the name,
  * which has repeated values in the lookup dataset ;
  set sashelp.class (keep=name);

  * find the first instance of the key ;
  _rc_h1=_h1.find();

  * find the remaining instances of the keys, ;
  * then apply further subsetting logic via the if statement ;
  * link out of the loop to derive all downstream variables ;
  * based on the lookup results. ;
  * we need an explicit output statement to only output the records ;
  * which match the subsetting criteria. ;
  do while (_rc_h1=0);
    if ("01JAN60"d le date le "15JAN60"d) then do;
      link derive;
      output;
    end;
    _rc_h1=_h1.find_next();
  end;

  * return;

  * if doing several multidata lookups, only the last lookup should include the return statement ;
  * normally this would be done with different lookup hash objects ;

  * need to do another find since at this point _rc_h1 ne 0 ;
  _rc_h1=_h1.find();

  do while (_rc_h1=0);
    if (age le 12) then do;
      link derive;
      output;
    end;
    _rc_h1=_h1.find_next();
  end;

  return;

  derive:
    height=height*1000;
    weight=age*1000;
    new=date;
    format new yymmddd.;
  return;
run;

proc print;
run;

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

Use %hash_define to load the lookup dataset(s).  This sets the
_hashnum_ macro variable used in this macro.

The return code from the find() command is returned in data step
variables _rc_h1 - _rc_h<n>, where "n" matches the number of times
%hash_define was called.  No error checking is done on the return
code from within this macro, but error checking could be done in
the calling code.

Alternatively, if the hashname parameter was used, the return code
from the find() command is returned as _rc_<hashname>, eg.
_rc_foo, _rc_bar, _rc_myhash, etc.

If the hashname has a leading underscore, only one underscore is used
in the return code.  In other words:
   If the hashname is myhash, the hash object is myhash.find(),
      and the return code is _rc_myhash.
   If the hashname is _myhash, the hash object is _myhash.find(),
      and the return code is _rc_myhash, NOT _rc__myhash.

By default a lookup will be done across all hash objects defined by the
%hash_define macro.  Use either the hashnum= or hashname= option
to limit the lookup to a single hash object.

If the LOOKUP parameter is specified:

  It must be syntactically correct subsetting criteria using if
  statement syntax which is used to further subset additional items
  which match the lookup key.

  You would normally also specify the HASHNUM or HASHNAME parameter,
  unless the logic criteria for all lookups was identical
  (highly unlikely).

  The HASHNUM associated with a LOOKUP parameter lookup must be
  associated with a MULTIDATA %hash_define statement.  Otherwise, you
  will get a runtime error when the find_next() method is executed
  against a non-multidata hash object.

  The LOOKUP= %hash_lookup macro call should be the LAST lookup
  specified, since by default it also adds the return statement to the
  generated code.

  If several multidata lookups are conducted in the same data step, only
  the final multidata %hash_lookup invocation should issue the return
  statement (RETURN=Y).

  If there are downstream derived variables that rely on the values
  returned from the lookups, then you should also specify the LINK
  parameter, which should be a data step label marking the start of your
  derived variables.

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

%macro hash_lookup
/*--------------------------------------------------------------------
Lookup satellite variables from a hash object
--------------------------------------------------------------------*/
(HASHNUM=      /* Limit lookup to a specific hash by number (Opt).  */
,HASHNAME=     /* Limit lookup to a specific hash by name (Opt).    */
,LOOKUP=       /* Perform a multidata item lookup? (Opt).           */
               /* If specified, then this criteria is used to       */
               /* perform addtional multidata item lookups          */
               /* and filter the results.                           */
,LINK=         /* Link to additional variable derivations? (Opt).   */
               /* If specified, it must be a data step link label   */
               /* defined outside this macro that marks the start   */
               /* of the additional variable derivations.           */
,RETURN=N      /* Issue a return statement? (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 _num_;
%let macro = &sysmacroname;

%* check input parameters ;
%if (not %symexist(_hashnum_)) %then
%parmv(_msg=%nrstr(The _hashnum_ global macro variable does not exist.  %hash_define must be called before calling this macro))

%if (&hashname ne ) %then
%if (%sysfunc(findw(&_hashname_,&hashname,|,IRST)) eq 0) %then
%parmv(_msg=%nrstr(The &hashname is not in the &_hashname_ list.  Please review the %hash_define invocations.))

%parmv(HASHNUM,      _req=0,_words=0,_val=POSITIVE)
%parmv(HASHNAME,     _req=0,_words=0,_case=N);
%parmv(LOOKUP,       _req=0,_words=1,_case=N)
%parmv(LINK,         _req=0,_words=0,_case=N)
%parmv(RETURN,       _req=1,_words=0,_case=U,_val=0 1)

/*
%if (&hashnum ne ) %then
%if (&hashnum gt &_hashnum_) %then
%parmv(_msg=The hashnum parameter is invalid);
*/

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

%* define utility macro to perform multidata lookups ;
%* since we are ONLY outputting when we have found a record, ;
%* we do not need to set the satellite variables to missing. ;
%* we know they must have been set when the lookup was successful. ;
%macro multidata_lookup(criteria);
  %* the initial lookup was successful ;
  do while (_rc&hn=0);
    if (&criteria) then do;
      %if (&link ne ) %then %do;
      link &link;
      %end;
      output;
    end;
    _rc&hn=&hn..find_next();
  end;
%mend;

%* perform lookup for each lookup table ;
%if (&hashname ne ) %then %do;
  %let hn = &hashname;
  %let rc = _rc;
  %if (%substr(&hn,1,1) eq _) %then
    %let rc=&rc.&hn;
  %else
    %let rc=&rc._&hn;
  &rc = &hn..find();

  %if (%superq(lookup) ne ) %then %do;
    %multidata_lookup(&lookup)
  %end;
%end;
%else
%if (&hashnum ne ) %then %do;
  %let hn = %scan(&_hashname_,&hashnum,|);
  %let rc = _rc;
  %if (%substr(&hn,1,1) eq _) %then
    %let rc=&rc.&hn;
  %else
    %let rc=&rc._&hn;
  &rc = &hn..find();

  %if (%superq(lookup) ne ) %then %do;
    %multidata_lookup(&lookup)
  %end;
%end;
%else
%do _num_=1 %to &_hashnum_;
  %let hn = %scan(&_hashname_,&_num_,|);
  %let rc = _rc;
  %if (%substr(&hn,1,1) eq _) %then
    %let rc=&rc.&hn;
  %else
    %let rc=&rc._&hn;
  &rc = &hn..find();

  %if (%superq(lookup) ne ) %then %do;
    %multidata_lookup(&lookup)
  %end;
%end;

%if (&return) %then %do;
  return;
%end;

%quit:

%mend;

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


text:
/*=====================================================================
Program Name            : time_interval.sas
Purpose                 : Create a metadata dataset of date, time, or
                          datetime values for a specified time interval.
SAS Version             : Unknown (probably SAS 8.2)
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 12FEB2008
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:

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

%macro time_interval
/*---------------------------------------------------------------------
Create a metadata dataset of date, time, or datetime values for a
specified time interval.
---------------------------------------------------------------------*/
(INTERVAL=     /* Specifies a time interval (REQ).  Valid values are */
               /* DAY, WEEK, WEEKDAY, MONTH, QTR, SEMIYEAR, YEAR,    */
               /* DTDAY, DTWEEK, DTWEEKDAY, DTMONTH, DTQTR,          */
               /* DTSEMIYEAR, DTYEAR, HOUR, MINUTE, SECOND.          */
               /* The time interval must match the type of the start */
               /* value.                                             */
,MULTIPLE=     /* Specifies a multiple of the interval (Opt).        */
               /* It sets the interval equal to a multiple of the    */
               /* interval type.  Valid values are NONNEGATIVE       */
               /* integers valid for the interval type.              */
,SHIFT=        /* Specifies the starting point of the interval (Opt).*/
               /* By default, the starting point is 1.  Valid values */
               /* are NONNEGATIVE integers valid for the interval    */
               /* type.                                              */
,START=        /* Starting date, time, or datetime value (REQ).      */
,END=          /* Ending date, time, or datetime value (Opt).        */
               /* Either END or INCREMENT must be specified, and are */
               /* mutually exclusive.  If both are specified, END    */
               /* takes precedence.                                  */
,INCREMENT=    /* Specifies a NONNEGATIVE integer that represents    */
               /* the number of date, time, or datetime intervals    */
               /* (REQ).  Increment is the number of intervals to    */
               /* shift the value of start from.                     */
,DIRECTION=B   /* Direction to shift the value of start from (REQ).  */
               /* Valid values are B BACKWARD F FORWARD.  Default    */
               /* value is BACKWARD, i.e. backwards in time from the */
               /* start value.                                       */
,ALIGNMENT=S   /* Controls the position of SAS dates within the      */
               /* interval (Opt).  Valid values are B BEGINNING      */
               /* M MIDDLE E END S SAME.  Default value is SAME.     */
               /* If not specified, BEGINNING is used.               */
,DATASET=TIME_INTERVAL
               /* Name of output dataset (REQ).  Default value is    */
               /* TIME_INTERVAL.                                     */
,MVAR=TIME_INTERVAL
               /* Macro variable to contain the time intervals (Opt).*/
               /* Default value is TIME_INTERVAL.  If not specified, */
               /* no macro variable is created.                      */
,FORMAT=YYMMDDN8.
               /* Format used to format the data in the dataset and  */
               /* macro variable (Opt).  Default value is YYMMDDN8.  */
               /* If not specified, no format is used, i.e. raw data.*/
               /* The parameter must be a valid format appropriate   */
               /* for the time interval specified.                   */
);

%local macro parmerr;

%* check input parameters ;
%let macro = &sysmacroname;
%parmv(INTERVAL,     _req=1,_words=0,_case=U,
                     _val=DAY WEEK WEEKDAY MONTH QTR SEMIYEAR YEAR
                          DTDAY DTWEEK DTWEEKDAY DTMONTH DTQTR DTSEMIYEAR DTYEAR
                          HOUR MINUTE SECOND)
%parmv(MULTIPLE,     _req=0,_words=0,_case=N,_val=NONNEGATIVE)
%parmv(SHIFT,        _req=0,_words=0,_case=N,_val=NONNEGATIVE)
%parmv(START,        _req=1,_words=1,_case=U)
%parmv(END,          _req=0,_words=1,_case=U)
%parmv(INCREMENT,    _req=0,_words=0,_case=N,_val=NONNEGATIVE)
%parmv(DIRECTION,    _req=1,_words=0,_case=U,_val=B BACKWARD F FORWARD)
%parmv(ALIGNMENT,    _req=0,_words=0,_case=U,_val=B BEGINNING M MIDDLE E END S SAME)
%parmv(DATASET,      _req=1,_words=0,_case=U)
%parmv(MVAR,         _req=0,_words=0,_case=U)
%parmv(FORMAT,       _req=0,_words=0,_case=U)

%* additional error checking ;
%* at least one of END or INCREMENT must be specified ;
%if (%quote(&end.&increment) eq ) %then %do;
   %parmv(_msg=END or INCREMENT must be specified)
%end;

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

%* if both END and INCREMENT are specified, print a warning, END takes precedence ;
%if (&end ne ) and (&increment ne ) %then %do;
   %put WARNING: END and INCREMENT are mutually exclusive.  END will be used.;
   %let increment = ;
%end;

%* build the interval string ;
%if (&multiple ne ) %then %let interval = &interval.&multiple;
%if (&shift    ne ) %then %let interval = &interval..&shift;

%* parse parameters that have an alias ;
%let direction = %substr(&direction,1,1);
%let alignment = %substr(%str(&alignment ),1,1);  /* use %str( ) in case alignment is blank */

%* build the metadata dataset ;
data &dataset;
   length n start end time_interval 8;
   call missing(of _all_);
   %if (&format ne ) %then %do;
   format start end time_interval &format;
   %end;
   start = &start;
   %if (&end ne ) %then %do;
   end   = &end;

   select("&direction");
      when("B") do;
         if start lt end then do;
            put "ERROR: START must be greater than or equal to END when direction is BACKWARD.";
            stop;
         end;
      end;
      when("F") do;
         if start gt end then do;
            put "ERROR: START must be less than or equal to END when direction is FORWARD.";
            stop;
         end;
      end;
   end;
   %end;

   n=0;
   do while (1);
      %if (&increment ne ) %then %do;
      if (n ge &increment) then leave;
      %end;
      time_interval=intnx(
         "&interval",
         &start,
         %if (&direction eq B) %then -;n
         %if (&alignment ne  ) %then ,"&alignment";
      );
      %if (&end ne ) %then %do;
         %if (&direction eq B) %then
            %let comp = lt;
         %else
            %let comp = gt;
      if (time_interval &comp &end) then leave;
      %end;
      output;
      n+1;
   end;
run;

%* if macro variable was specified, return the data as a string ;
%if (&mvar ne ) %then %do;
%global &mvar;
proc sql noprint;
   select time_interval into :&mvar separated by " " from &syslast;
quit;
%put >>> &mvar=&&&mvar;
%end;

%quit:
%mend;

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

repo_name: scottbass/SAS

repo_language: SAS

mime_type: text/plain


text:
/*******************************************************************************************

          Program:  txt2pdf.sas
      Description:  Convert text files to Adobe(TM) Acrobat (PDF) documents
           Inputs:  Macro parameters
         Calls to:  None
      Usage Notes:  The source text file is paginated based on form-feed characters (unix,
                    windows platforms), or newpage line attribute (MVS platforms).  If your
                    text file does not contain these characters, you will only get one page
                    in the resulting PDF (if not an error).

                    If you are producing output in a data _null_ step, the output mode
                    should be set to "print", as in:

                        data _null_;
                          file 'my_output_file.txt' print;
                          ...

                    Although the output file created by %m_txt2pdf is made up of printable
                    characters, PDF is technically a binary format.  Furthermore, the exact
                    offset of each page in the file is marked in the cross-reference table,
                    so any modification of the output file (by a text editor, for example)
                    that will change the number of bytes in the output file will invalidate
                    the cross-reference table and may make your PDF document unreadable.

                    For convenience of debugging, the macro produces output in the native
                    character set of host platform.  The charset and crlf parameters are
                    provided to allow tune the file output for cross-platform transfers in
                    ASCII mode.  If you are generating and viewing your PDF documents on the
                    same platform, these parameters can be left unspecified.

                    Setting the margins:

                             ^  +------- Dimensions of the page ------------------------+
                             |  |                                     ^                 |
                             |  |                                     | TM              |
                             |  |                                     v                 |
                             |  |      +----- Printing Area ---------------------+      |
                             |  |<---->|                                         |<---->|
                             |  |  LM  |                                         |  RM  |
                          PH |  |      |                                         |      |
                             |  |      |                                         |      |
                             |  |      |                                         |      |
                             |  |      |                                         |      |
                             |  |      |                                         |      |
                             |  |      +-----------------------------------------+      |
                             |  |         ^                                             |
                             |  |         | BM                                          |
                             |  |         v                                             |
                             v  +-------------------------------------------------------+
                                 <-------------------- PL ----------------------------->
                                             Landscape  Portrait
                         PH = Page Height -    8.5        11       612  792
                         PL = Page Length -   11           8.5     792  612
                         BM = Bottom Margin
                         TM = Top Margin
                         LM = Left Margin
                         RM = Right Margin

        Revisions:  (date/by/reason)
          $Author:  $
        $Revision:  $
            $Date:  $
          $Source:  $
        $Comments:  $

*******************************************************************************************/

%macro txt2pdf(
     in        =    ,    /* Filename (in quotes), or fileref, of the input text file      */
     out       =    ,    /* Filename (in quotes), or fileref, of the PDF output file      */
     infile    =    ,    /* Alias for IN=                                                 */
     file      =    ,    /* Alias for OUT=                                                */
     ls        =    ,    /* Number of characters per line. If not specified then use      */
                         /* maximum non-blank linesize from the input text file.          */
     ps        =    ,    /* Number of lines per page. If not specified then use from      */
                         /* the input text file.                                          */
     fs        =    ,    /* Fontsize to use in PDF file. When not specified then an       */
                         /* appropriate value is calculated from LS and PS.               */
     minfs     = 6  ,    /* MINFS and MAXFS set the limits when calculating Fontsize      */
     maxfs     = 12 ,    /* from PS and LS.                                               */
     filenfs   =    ,    /* Fontsize to use in filename footer (0 means no filename)      */
                         /* Default to 0 for single input physical file, 6 otherwise.     */
     wm        =    ,    /* Text to appear as watermark on every page.                    */
     wmfs      = 128,    /* Fontsize to use for watermark. Default works well when        */
                         /* WM=DRAFT is used.                                             */
     charset   =    ,    /* Specifies the character set to use for output. Used to        */
                         /* set characters for the left and right square brackets []      */
                         /* This parameter can usually be left unspecified, unless        */
                         /* the output file is intended to be transferred between         */
                         /* platforms.  The following characters sets are defined:        */
                         /*    EBCDIC - (Default on MVS platforms)                        */
                         /*    ASCII - (Default for everything else)                      */
     crlf      =    ,    /* Specifies the number of characters in a "newline" on          */
                         /* this platform.  The default is 2 for Windows ('0D0A'x),       */
                         /* 2 for MVS (anticipating transfer to windows platform),        */
                         /* and 1 for everything else ('0A'x, i.e. unix systems)          */
     cc        =    ,    /* Interpret first column as FORTRAN Carriage Control codes?     */
                         /* (0/1).  Control characters support are: 1=Formfeed,           */
                         /* 0=Double space, -=Triple space. First column is stripped      */
                         /* from the output. When CHARSET=EBCDIC then default for CC      */
                         /* is 1 (yes) otherwise the default is 0 (no) .                  */

     /* Page dimensions and orientation.                                                  */
     orient    = L  ,    /* Orientation Landscape or Portrait                             */
     tm        = 1  ,    /* Top Margin in Inches                                          */
     bm        = 1  ,    /* Bottom Margin in Inches                                       */
     lm        = 1  ,    /* Left Margin in Inches                                         */
     rm        = 1  ,    /* Right Margin in Inches                                        */
     fontstep  = 4  ,    /* Font size ganularity. Calculate fontsize to the best          */
                         /* 1/fontstep unit. For example 4 means quarter of a font        */
                         /* size (12 11.75 11.50 11.25 11 ...)                            */

     /* PDF Document information fields                                                   */
     author    =    ,    /* Windows default %USERNAME%. Unix default id command.          */
     title     = 5  ,    /* Title. Text or Number. Number means nth nonblank line         */
     subject   =    ,    /* Subject.                                                      */

     /* Where to get Bookmark information.                                                */
     bookmk    = 4  ,    /* Bookmark text to insert pointing to first page.               */
                         /* BOOKMK=nnn  Use non-blank line number nnn. Generate a new mark*/
                         /*             everytime the value changes in the input file(s). */
                         /* BOOKMK=name When &BOOKMK is the name of a dataset with fields */
                         /*             PAGE and BKMARK then use that dataset as source   */
                         /*             of marks.                                         */
                         /* Otherwise make a single book mark of value &BOOKMK to page 1. */
     );
%*=========================================================================

 SAS2PDF v0.03

   A SAS macro for converting text files to PDF documents

 Copyright (C) 2000-2003  Information Softworks

 This program is free software, you can redistribute it and/or
 modify it under the terms of the GNU General Public License
 as published by the Free Software Foundation, either version 2
 of the License, or (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY, without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

 For information, contact:
   Information Softworks
   2181 Stonebridge Drive
   Ann Arbor, MI 48108

   or email:  sas2pdf@informationsoftworks.com

 You should have received a copy of the GNU General Public License
 along with this program if not, write to the Free Software
 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

---------------------------------------------------------------------------
v0.03 - P Wehr
   Add missing keyword to Remove annoying "this file has been damaged, but
is being repaired" message from Acrobat reader.  Thanks to Duncan Hon
for his keen eye.
---------------------------------------------------------------------------
v0.02 - P Wehr
   Brown Paper Bag bug fix--removed substring that trimmed off the first
character for all OSs except MVS, and it doesnt even print the first page
on most platforms...

===========================================================================;

%local macro parmerr i
 validds            /* Used to test if &BOOKMK is a dataset name           */
 dsname             /* Dataset Name to use in bookmark generation          */
 lb rb              /* Left and right square brackets                      */
 pages files marks  /* Input Counters                                      */
 objects            /* Used to dimension XREF table offset storage array   */
 maxchar            /* Adjust for version 6 limitations                    */
 pagechar           /* New page character                                  */

 /* Variables used in setting font                                         */
 maxps minps maxls vfs hfs long short
 pl                 /* Text length in inches                               */
 ph                 /* Text height in inches                               */
 tl                 /* Text length in pica (1/120 in)                      */
 th                 /* Text height in page units (1/72 in)                 */
 titlen
 pad                /* Adjust length of XREF table lines                   */

 /* Variables used in generating PDF motion commands                       */
 origin             /* Rotation matrix and where to put first line         */
 vm                 /* How far to move to start next line                  */
 fnorig             /* Where to put filename footer                        */
;

%let macro=&sysmacroname;

%if (%sysevalf(&sysver >= 7)) %then %let maxchar=1000;
%else %let maxchar=200;

%let orient=%upcase(%substr(&orient.L,1,1));

%*----------------------------------------------------------------------
Get a value of the AUTHOR info tag
-----------------------------------------------------------------------;
%if ^%length(&author) %then %do;
  %if &sysscp=WIN %then %let author = %sysget(USERNAME);
  %else %let author = %upcase(&sysuserid);
%end;

%*----------------------------------------------------------------------
Either set TITLEN to number specified in TITLE parameter or set to 0
when TITLE parameter is not a number.
-----------------------------------------------------------------------;
%if %sysfunc(verify(&title,0123456789)) %then %do;
  %let titlen=0;
%end;
%else %do;
  %let titlen=&title;
  %let title=SAS output;
%end;

%*----------------------------------------------------------------------
Check how to get bookmarks.
-----------------------------------------------------------------------;
%if ^%sysfunc(verify(&bookmk,0123456789)) %then %do;
%*----------------------------------------------------------------------
When &BOOKMK is an integer then scan input for bookmark text.
-----------------------------------------------------------------------;
  %let dsname=_bkmark_(keep=page bkmark filename);
%end;
%else %do;
%*----------------------------------------------------------------------
Check if &BOOKMK is in format for a valid dataset name. (This will
prevent an error message from the OPEN command used by VAREXIST.) If
format is ok then test if required PAGE and BKMARK variables exist.
-----------------------------------------------------------------------;
  %let validds=%qscan(&bookmk,1,%str(%());
  %if (%length(%qscan(&validds,1,.))>32)
   or (%length(%qscan(&validds,2,.))>32)
   or (%length(%qscan(&validds,3,.))>0 )
   or (%index(&validds,..))
   %then %let validds=0;
  %else %if ((%varexist(&bookmk,PAGE,TYPE)=N) and
            (%varexist(&bookmk,BKMARK,TYPE)=C)) %then %do;
      %let validds=1;
  %end;
  %else %let validds=0;

*----------------------------------------------------------------------;
* Create BOOKMARK dataset _BKMARK_ ;
*----------------------------------------------------------------------;
    data _bkmark_ (keep=page bkmark);
      length page 8 bkmark $ &maxchar;
  %if (&validds) %then %do;
*----------------------------------------------------------------------;
* Use the dataset named by BOOKMK parameter. ;
*----------------------------------------------------------------------;
      set &bookmk end=eof;
      if eof then call symput('MARKS',compress(put(_n_,8.)));
  %end;
  %else %do;
*----------------------------------------------------------------------;
* Use the BOOKMK parameter as bookmark to page one. ;
*----------------------------------------------------------------------;
      page=1;
      bkmark="&bookmk";
      call symput('MARKS','1');
  %end;
    run;
%*----------------------------------------------------------------------
Set dataset name to _NULL_ and BOOKMK to zero to prevent bookmark
detection during first pass reading of the input.
-----------------------------------------------------------------------;
  %let dsname=_null_;
  %let bookmk=0;
%end;

%let charset=%upcase(&charset);
%if %bquote(&in)=%bquote() %then %let in=&infile;
%if %bquote(&out)=%bquote() %then %let out=&file;
%if &sysscp=WIN %then %do;
  %if &crlf= %then %let crlf=2;
  %if &charset= %then %let charset=ASCII;
  %if &cc= %then %let cc=0;
%end;
%else %if &sysscp=OS %then %do;
  %if &crlf= %then %let crlf=2;
  %if &charset= %then %let charset=EBCDIC;
  %if &cc= %then %let cc=1;
%end;
%else %do;
  %if &crlf= %then %let crlf=1;
  %if &charset= %then %let charset=ASCII;
  %if &cc= %then %let cc=0;
%end;
%if (&crlf=1) %then %let pad=%str( );

%if (&cc) %then %let pagechar='1';
%else %let pagechar='0C'x;

%*-----------------------------------------------------------------------
Generate left and right bracket characters.  Brackets do not pass
through some ASCII <-> EBCDIC conversions correctly.
  ASCII  uses 5B and 5D hex
  EBCDIC uses AD and BD hex
------------------------------------------------------------------------;
%if &charset=EBCDIC %then %do;
  %let lb=%sysfunc(byte(173));
  %let rb=%sysfunc(byte(189));
%end;
%else %do;
  %let lb=%sysfunc(byte(91));
  %let rb=%sysfunc(byte(93));
%end;

*-----------------------------------------------------------------------;
* Scan input file and determine number of pages, maximum linesize,  ;
* maximum pagesize, table number (bookmark) and title. ;
* Create dataset of bookmarks to pages. ;
*-----------------------------------------------------------------------;
data &dsname;
  length page 8 line bkmark filename fname oldbk $ &maxchar;
  retain maxls 0 page 0 ps 0 maxps 0 titlen 0 files 0 marks 0 oldbk;
  infile &in end=eof length=len eov=eov filename=fname lrecl=&maxchar;
  input line $varying&maxchar.. len;
  filename = fname;
  formfeed = (line =: &pagechar);
  linelen = length(line)-(formfeed or &cc);
  if formfeed or _n_=1 or eov then do;
    page+1;
    if ps > maxps then maxps = ps;
    ps = 0;
        titlen = 0;
    if _n_=1 or eov then do;
      files+1;
    end;
  end;
  eov=0;
  if (line ^=: '1B'x) and (line ^= &pagechar) then do;
    ps+1;
    %if (&cc) %then %do;
      if (line =: '0') then ps+1;
      if (line =: '-') then ps+2;
    %end;
    if linelen > maxls then maxls=linelen;
    if substr(line,1+(formfeed or &cc)) ne ' ' then do;
      titlen+1;
*-----------------------------------------------------------------------;
* Look for text to use for the bookmark/table number ;
*-----------------------------------------------------------------------;
      if (titlen=&bookmk) then do;
        bkmark=left(substr(line,1+(formfeed or &cc)));
        bkmark=substr(bkmark,1,index(bkmark,'  ')-1);
        if (bkmark ne oldbk) then do;
          marks+1;
          output;
          oldbk=bkmark;
        end;
      end;
*-----------------------------------------------------------------------;
* Look for text to use for the title  ;
*-----------------------------------------------------------------------;
      if (page=1) and (titlen=&titlen) then do;
        bkmark=left(substr(line,1+(formfeed or &cc)));
        if index(upcase(bkmark),'PAGE ') then do;
          bkmark=substr(bkmark,1,index(upcase(bkmark),'PAGE ')-1);
        end;
        call symput('title',trim(bkmark));
      end;
    end;
  end;
  if eof then do;
    if ps > maxps then maxps = ps;
    call symput('files',compress(put(files,8.)));
    call symput('pages',compress(put(page,8.)));
    call symput('maxls',compress(put(maxls,8.)));
    call symput('maxps',compress(put(maxps,8.)));
%if (&bookmk) %then %do;
    call symput('marks',compress(put(marks,8.)));
%end;
  end;
run;

%put NOTE: &macro Files=&files Pages=&pages Pagesize=&maxps Linesize=&maxls;

%*-----------------------------------------------------------------------
Set PS and LS based on data if not specified in macro call
------------------------------------------------------------------------;
%if (&ps = ) %then %let ps=&maxps;
%if (&ls = ) %then %let ls=&maxls;

%*-----------------------------------------------------------------------
When FILENFS not specified default to 6 for multiple files
------------------------------------------------------------------------;
%if (&filenfs = ) %then %do;
  %if (&files = 1) %then %let filenfs=0;
  %else %let filenfs=6;
%end;

*--------------------------------------------------------------------;
* Set dimensions ;
*--------------------------------------------------------------------;
%let long=11;
%let short=8.5;
%if (&orient=P) %then %do;
  %let ph=&long;
  %let pl=&short;
  %let origin=%sysevalf(&tm*72) %sysevalf((&lm)*72);
  %let fnorig=0 1 -1 0 %sysevalf((&long-&bm)*72-&filenfs) %sysevalf(&lm*72);
%end;
%else %do;
  %let pl=&long;
  %let ph=&short;
  %let origin=%sysevalf(&lm*72) %sysevalf((&short-&tm)*72);
  %let fnorig=1 0 0 1 %sysevalf(&lm*72) %sysevalf(&bm*72);
%end;
%let tl=%sysevalf((&pl-&lm-&rm)*120);
%let th=%sysevalf((&ph-&tm-&bm)*72);

%*--------------------------------------------------------------------
Center the WM on the page
*--------------------------------------------------------------------;
%if %length(&wm) %then %do;
   %let diag = %sysfunc(sqrt(&ph*&ph+&pl*&pl));
   %let wmlen=%length(&wm);
   %if (&wmfs=) %then %do;
      %let wmfs=%sysfunc(int(&diag*120/(&wmlen+1)));
   %end;
   %let wmls=%sysfunc(ceil(&diag*120/&wmfs));
   %if (&wmls > &wmlen) %then %let wm=%qsysfunc(repeat(%str( ),(&wmls-&wmlen-1)/2))&wm;
   %put NOTE: Watermark calculations: wmlen=&wmlen wmls=&wmls diag=&diag wmfs=&wmfs wm="&wm";
%end;

%*-----------------------------------------------------------------------
Determine fontsize to use based on maximum linesize
Determine fs needed for LS and PS to two decimal places.
Take the smaller of the two, but restrict to between MINFS and MAXFS.
------------------------------------------------------------------------;
%if (&fs = ) %then %do;
%* Calculate FS to fit linesize in 9 inches ;
  %let hfs=%sysfunc(int(&tl*&fontstep/&ls));
  %let hfs=%sysfunc(putn(&hfs/&fontstep,5.2));
%* Calculate FS to fit pagesize in 6.5 inches ;
  %let vfs=%sysfunc(int(&th*&fontstep/&ps));
  %let vfs=%sysfunc(putn(&vfs/&fontstep,5.2));
  %let fs=%sysfunc(min(&maxfs,&vfs,&hfs));
  %let fs=%sysfunc(max(&minfs,&fs));

%put NOTE: &macro Fontsize calculations: Linesize=&ls F=&hfs Pagesize=&ps F=&vfs Fontsize=&fs ;

%end;

%*----------------------------------------------------------------------
Make sure PS used in placing lines is at least as large as is needed
by maximum fontsize so that lines are not widely separated in short
files. Calculate the vertical motion based on PS.
-----------------------------------------------------------------------;
%let minps=%sysfunc(ceil(&th/&maxfs));
%let ps=%sysfunc(max(&ps,&minps));
%let vm=%sysfunc(putn((&th-&filenfs)/&ps,8.2));

%*----------------------------------------------------------------------
Calculate: the number of PDF "objects" to use in array definition.
-----------------------------------------------------------------------;
%let objects=%eval(2*&pages + 7 + 2*&marks);

%*---------------------------------------------------------------------
PDF files are a series of objects.  At the end of the files is a
a cross reference table that points to the beginning of each object in
the file.  To be able to generate the xref table we need to keep track
of how many characters are in each line.  So each put statement must
end with trailing @ and then subroutine is called to put end-of-line
and count number of bytes.
*----------------------------------------------------------------------;

*-----------------------------------------------------------------------;
* Read input file and generate PDF file. ;
*-----------------------------------------------------------------------;
data _null_;
  length string instring filename fname $ &maxchar;
  file &out noprint lrecl=32000 column=col;
  infile &in length=lg end=eof filename=fname eov=eov lrecl=&maxchar;
  retain bytecnt obj 0 lines 0 lineone 1 filename page1 info  ;
  array objbyte (&objects) 8 _temporary_;
  array line (%eval(&maxps+9)) $ &maxchar _temporary_;

  if _n_=1 then do;
    filename=fname;
    put '%PDF-1.3' @;        link output;
    put '25E2E3CFD3'x @;
    link object;
    put '<< /Type /Catalog /Pages 6 0 R /Outlines 2 0 R /PageMode /UseOutlines >>' @;
    link object;
    put "<< /Type /Outlines /Count " marks @;
    if (marks) then do;
      firstmk = 7+2*&pages+2;
      lastmk = 7+2*&pages+2*marks;
      put '/First ' firstmk '0 R /Last ' lastmk '0 R ' @;
    end;
    put '>>' @;
    link object;
    put '<< /Type /Font /Subtype /Type1 /Name /F1'
        ' /BaseFont /Courier /Encoding /WinAnsiEncoding >>' @;
    link object;
    put '<< /Type /Font /Subtype /Type1 /Name /F2'
        ' /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>' @;
    link object;
    put "&lb /PDF /Text &rb" @;
    link object;
    page1=obj+2;
    put "<< /Type /Pages /Count &pages /Kids &lb" @;
    link output;
    do page=page1 to (page1+2*(&pages-1)) by 2;
      put page '0 R' @;
      link output;
    end;
    put "&rb >>" @;
    link output;
*-----------------------------------------------------------------------;
* Document Info ;
*-----------------------------------------------------------------------;
    link object;
    info=obj;
    string="&title"; link protect;
    put '<< /Title (' string +(-1) ')' @; link output;
    string="&subject"; link protect;
    put '/Subject (' string +(-1) ')' @; link output;
    put "/Author (&author)" @; link output;
    put '/Keywords (SAS)' @; link output;
    put "/Creator (&macro)" @; link output;
    put "/Producer (&macro)" @; link output;
    date  =tranwrd(compress(
            put(date(),yymmdd10.) !! put(time(),time8.),':-')
           ,' ','0');
    put '/ModDate (D:' date  +(-1) ')' @; link output;
    put '/CreationDate (D:' date +(-1) ')' @; link output;
    put "/OpenAction &lb " page1 "0 R /FitVisible &rb >>" @; link output;
%if %length(&wm) %then %do;
*--------------------------------------------------------------------;
* Calculate transition matrix for when WM is specified ;
* Rotate by angle A use "cos(A) sin(A) -sin(A) cos(A) 0 0 Tm" ;
*--------------------------------------------------------------------;
   length wmts wmtm $ 200;
   retain wmts wmtm ;
%if (&orient=P) %then %do;
   r=atan((&pl-&lm-&rm)/(&ph-&tm-&bm) );
%end;
%else %do;
   r=atan(-(&ph-&tm-&bm)/(&pl-&lm-&rm) );
%end;
   wmts=compress(put(-&wmfs*.35,10.6));
   wmtm=compbl(put(cos(r),10.6)!!put(sin(r),10.6)!!put(-1*sin(r),10.6)!!put(cos(r),10.6));
%end;

  end;
  input instring $varying. lg;
  if ('1b'x ^=: instring) then do;
    formfeed = (instring =: &pagechar);
    if formfeed or lineone or eov then do;
      eov=0;
      if lineone then lineone=0;
      else link endpage;
*----------------------------------------------------------------------;
* Write the beginning of a page. First create the Page Object with     ;
* indirect pointer to the next object that will contain the Stream that;
* is the actual contents of the page. The second object will be written;
* when end of page is reached so that /Length can be calculated.       ;
*----------------------------------------------------------------------;
      link object;
      next=obj+1;
      put '<< /Type /Page /Parent 6 0 R /Resources '
          '<< /Font << /F1 3 0 R /F2 4 0 R >> /ProcSet 5 0 R >> '
%if (&orient=P) %then
          '/Rotate 90 '
;
          "/MediaBox &lb 0 0 792 612 &rb /Contents "
          next ' 0 R >>' @
      ;
      link output;
      lines=1;
%if (%length(&wm)) %then %do;
%* Rotate by angle A use "cos(A) sin(A) -sin(A) cos(A) 0 0 Tm" ;
      line(lines)= "BT /F2 &wmfs Tf " !! trim(wmtm) !! " &origin Tm "
       /* !! "-%eval(&wmfs/2) Ts " */ !! trim(wmts) !! " Ts " !! "0 Tr 0.9 g (&wm)' 0 Ts ET ";
      lines+1;
%end;
%if (&orient=P) %then %do;
      line(lines)= "BT /F1 &fs Tf &vm TL 0 1 -1 0 &origin Tm 0 Ts 0 Tr 0 g";
%end;
%else %do;
      line(lines)= "BT /F1 &fs Tf &vm TL 1 0 0 1 &origin Tm 0 Ts 0 Tr 0 g";
%end;
    end;
    if instring ne &pagechar then do;
*-----------------------------------------------------------------------;
* Count this line ;
*-----------------------------------------------------------------------;
      lines+1;
  %if (&cc) %then %do;
*-----------------------------------------------------------------------;
* Insert blank lines for double and triple space carriage control. ;
*-----------------------------------------------------------------------;
      if (instring =: '0') then do;
        line(lines)="( )'";
        lines+1;
      end;
      if (instring =: '-') then do;
        line(lines)="( )' ( )'";
        lines+1;
      end;
  %end;
*-----------------------------------------------------------------------;
* Save line into the array for printing at the end of the page. ;
*-----------------------------------------------------------------------;
      string=substr(instring,1+(formfeed or &cc));
      link protect;
      line(lines)='(' !! trim(string) !! ")'";
    end;
  end;
  if eof then do;
    link endpage;
*-----------------------------------------------------------------------;
* Bookmarks take two objects each. One for action and one for text. ;
*-----------------------------------------------------------------------;
    do i=1 to marks;
      set _bkmark_ nobs=marks;
      link object;
      action=obj;
      next=obj+3;
      prev=obj-1;
      target=page1+(page-1)*2;
      put "<< /S /GoTo /D &lb " target "0 R /XYZ &origin null &rb >>" @;
      link object;
      string=bkmark; link protect;
      put '<< /Title (' string +(-1) ')' @;
      if (1 <= i < marks) then put ' /Next ' next '0 R' @;
      if (marks >= i > 1) then put ' /Prev ' prev '0 R' @;
      put ' /Parent 2 0 R /A ' action '0 R >>' @; link output;
    end;

*-----------------------------------------------------------------------;
* Write cross reference table. ;
*-----------------------------------------------------------------------;
%*----------------------------------------------------------------------
NOTE: Can use PUT as no longer need to count bytes.
When CRLF = 1 need to add a space to end of lines to make 20 bytes.
------------------------------------------------------------------------;

    put 'endobj' @;   link output;
    obj+1;
    put 'xref'
      / '0 ' obj
      / "0000000000 65535 f&pad"
    ;
    do i=1 to obj-1;
      put objbyte(i) z10. " 00000 n&pad";
    end;
    put 'trailer'
      / '<< /Size ' obj '/Info ' info '0 R /Root 1 0 R >>'
      / 'startxref'
      / bytecnt
      / '%%EOF'
    ;
  end;
return;

protect:;
*-----------------------------------------------------------------------;
* Protect parenthesis and backslash. Note backslash must be first. ;
*-----------------------------------------------------------------------;
  string=tranwrd(string,'\','\\');
  string=tranwrd(string,'(','\(');
  string=tranwrd(string,')','\)');
return;

endpage:;
*-----------------------------------------------------------------------;
* Write page to output file. ;
*-----------------------------------------------------------------------;
  lines+1;
  line(lines)='ET';
%if (&filenfs) %then %do;
  lines+1;
  string = filename ; link protect;
  line(lines) = "BT /F2 &filenfs Tf &fnorig Tm 0 Tr 0 g";
  lines+1;
  line(lines) = '(' !! trim(string) !! ")' ET";
%end;
  pagesize=0;
  do i = 1 to lines;
    pagesize+length(line(i))+&crlf;
  end;
  link object;
  put '<< /Length ' pagesize '>>' @;  link output;
  put 'stream' @;    link output;
  do i=1 to lines;
    len=length(line(i));
    put line(i) $varying&maxchar.. len @;
    link output;
  end;
  put 'endstream' @; link output;
  filename = fname;
return;

output:;
*-----------------------------------------------------------------------;
* Write string to output file keeping track of number of bytes. ;
*-----------------------------------------------------------------------;
  bytecnt+(col-1)+&crlf;
  put ;
return;

object:;
*--------------------------------------------------------------------;
* Start a new object. Output end of last object and keep track of
* start location.;
*--------------------------------------------------------------------;
  if (col > 1) then link output;
  if obj then do;
    put 'endobj' @; link output;
  end;
  obj+1;
  objbyte(obj)=bytecnt;
  put obj '0 obj' @;
  *link output;
return;

run;

%mend;


text:
/*=====================================================================
Program Name            : empty.sas
Purpose                 : Checks if the source dataset is empty.
SAS Version             : SAS 9.4
Input Data              : N/A
Output Data             : N/A

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 04MAR2016
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 datasets ;
data empty not_empty;
   set sashelp.shoes (obs=30);
   output not_empty;
run;

* create test views ;
data not_empty_dsv / view=not_empty_dsv;
   set not_empty;
run;
data empty_dsv / view=empty_dsv;
   set empty;
run;

proc sql;
   create view not_empty_sql as
      select * from not_empty
   ;
   create view empty_sql as
      select * from empty
   ;
quit;

%put %empty(data=empty);         * should be 1 ;
%put %empty(data=not_empty);     * should be 0 ;

%put %empty(data=empty_dsv);     * should be 1 ;
%put %empty(data=not_empty_dsv); * should be 0 ;

%put %empty(data=empty_sql);     * should be 1 ;
%put %empty(data=not_empty_sql); * should be 0 ;

%put %empty(data=db_core.fsc_party_dim); * should be 0, works with Oracle tables too ;

%put %empty(data=not_empty(where=(region='XXX'))); * should be 1, no obs match where clause ;
%put %empty(data=doesnotexist);
%put %empty(data=doesnotexist(where=(region='XXX')));

data _null_;
   if %empty(data=empty) then
      put "Empty is empty";
   else
   if %empty(data=not_empty) then
      put "Not_empty is empty";
run;

%macro test;
   %let source=empty;
   %let empty=%empty(data=&source);
   %if &empty %then %put &source is empty.;

   %let source=not_empty;
   %let empty=%empty(data=&source);
   %if &empty %then %put &source is empty.;
%mend;
%test;

-----------------------------------------------------------------------
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 %empty(data=foo).

Returns 1 if the dataset/view/table is empty, 0 if not empty.
So, the programming logic is
"if <empty> then <empty processing>" or
"if not <empty> then <not empty processing>".

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

%macro empty
/*---------------------------------------------------------------------
Checks if the source dataset is empty.
---------------------------------------------------------------------*/
(DATA=         /* Input dataset or view (REQ).                       */
);

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

%* check input parameters ;
%parmv(DATA,         _req=1,_words=1,_case=N)  /* words=1 allows data set options */

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

%let pos=%index(&data,%str(%());
%if (&pos eq 0) %then %do;
   %let dataset=&data;
   %let options=;
%end;
%else %do;
   %let dataset=%substr(&data,1,&pos-1);
   %let options=%substr(&data,&pos);
%end;

%* additional error checking ;
%* check if the dataset exists ;
%if not (%sysfunc(exist(&dataset,data)) or %sysfunc(exist(&dataset,view))) %then %do;
   %parmv(_msg=&dataset does not exist)
   %goto quit;
%end;

%let dsid=%sysfunc(open(&dataset &options,I));
%if (&dsid le 0) %then %do;
   %parmv(_msg=Unable to open &dataset for input)
   %goto quit;
%end;
%let rc=%sysfunc(fetch(&dsid));
%let dsid=%sysfunc(close(&dsid));
%eval(not (&rc eq 0))

%quit:

%mend;

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


text:
/*=====================================================================
Program Name            : lock.sas
Purpose                 : Obtain or clear a dataset lock.
SAS Version             : SAS 9.2
Input Data              : None
Output Data             : None

Macros Called           : parmv
                          get_data_attr
                          handle

Originally Written by   : Michael Dixon
Date                    : 05JAN2010
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              : Michael Dixon
Date                    : 10AUG2010
Change/reason           : Remove Data Step
Program Version #       : 1.1

Programmer              : Scott Bass
Date                    : 19OCT2011
Change/reason           : Added support for SPDE libraries, changed to
                          use inner utility macros.
Program Version #       : 1.2

Programmer              : Scott Bass
Date                    : 29NOV2011
Change/reason           : Added call to %handle macro if lock fails.
                          Handle output echoed in SAS log.
Program Version #       : 1.3

Programmer              : Scott Bass
Date                    : 07MAY2013
Change/reason           : Added additional parameters to %handle macro
                          invocation, changed the timing of the call to
                          the %handle macro.  Added unlock and email
                          parameters to %lock, which are passed to
                          %handle.  Fixed logic error where &syscc was
                          not properly preserved when handle was called
                          on a SPDE dataset.
Program Version #       : 1.4

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

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

%lock(member=sashelp.cars)

Obtain a lock on sashelp.cars

%lock(member=sashelp.cars, action=clear)

Clear a lock on sashelp.cars

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

This macro was originally written by Michael Dixon.

I have modified it extensively to add support for SPDE datasets, calling
the handle.exe utility, and sending an optional email when a locked
dataset is encountered.

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

%macro lock
/*---------------------------------------------------------------------
Obtain or clear a dataset lock.
---------------------------------------------------------------------*/
(MEMBER=       /* Dataset on which to obtain or clear a lock (REQ).  */
,TIMEOUT=120   /* Timeout period in seconds on which to abort        */
               /* attempting to obtain a lock (REQ).                 */
,RETRY=2       /* Number of seconds to sleep between attempts to     */
               /* obtain a lock (REQ).                               */
,ACTION=LOCK   /* Action to take (REQ).                              */
               /* Valid values are LOCK or CLEAR.                    */
,ONFAIL=STOP   /* Action to take when a lock cannot be obtained (REQ)*/
               /* Valid values are STOP or SYSCC.                    */
               /* If STOP, the %abort cancel statement is submitted  */
               /* to abort the calling job.                          */
               /* If SYSCC, the &syscc macro variable is set for     */
               /* further processing by the calling job.             */
,UNLOCK=YES    /* Unlock a locked dataset? (Opt).                    */
               /* This parameter is passed to the %handle macro.     */
               /* Valid values are YES or NO.                        */
               /* If YES, the %handle macro will close any open      */
               /* handles on the dataset, unlocking it so processing */
               /* can continue in the calling program.               */
,EMAIL=YES     /* Send an email if open handles are closed? (Opt.)   */
               /* This parameter is passed to the %handle macro.     */
               /* Valid values are YES or NO.                        */
               /* If YES, the %handle macro will send an email to    */
               /* interested parties informing them that a database  */
               /* was locked and who was locking it.                 */
);

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

%* check input parameters ;
%parmv(MEMBER,       _req=1,_words=0,_case=N)
%parmv(TIMEOUT,      _req=1,_words=0,_case=U,_val=POSITIVE)
%parmv(RETRY,        _req=1,_words=0,_case=U,_val=POSITIVE)
%parmv(ACTION,       _req=1,_words=0,_case=U,_val=LOCK CLEAR)
%parmv(ONFAIL,       _req=1,_words=0,_case=U,_val=STOP SYSCC)
%parmv(UNLOCK,       _req=0,_words=0,_case=U,_val=YES NO)
%parmv(EMAIL,        _req=0,_words=0,_case=U,_val=YES NO)

%* parse off any dataset options ;
%let dsname=%scan(%superq(member),1,%str(%());

%* additional error checking ;
%* does the dataset exist? ;
%if (^%sysfunc(exist(&dsname,data)) and ^%sysfunc(exist(&dsname,view))) %then %do;
   %parmv(_msg=&dsname does not exist)
   %goto quit;
%end;

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

%global handle_rc;
%let handle_rc=0;

%* create inner utility macros ;
%macro lock_base(member=,timeout=,retry=,action=,onFail=,unlock=,email=);
   %let action=%upcase(&action);
   %let onfail=%upcase(&onfail);

   %local starttime;
   %let starttime = %sysfunc(datetime());
   %if (&action eq LOCK) %then %do;
      lock &member;
      %if &syslckrc LE 0 %then %do;
         %return;
      %end;
      %else
      %do %until(%sysevalf(%sysfunc(datetime()) gt (&starttime + &timeout)));
         %let SLEPT=%sysfunc(sleep(&retry));
         %put NOTE:: Trying to lock %upcase(&member)...;
         lock &member;
         %if &syslckrc LE 0 %then %do;
            %return;
         %end;
      %end;
      %* At this point, we have timed out ;
      %* Use the %handle macro to list and then close file locks, ;
      %* and try one last time to lock the member ;
      %handle(data=&member,unlock=&unlock,email=&email)
      lock &member;
      %if &syslckrc LE 0 %then %do;
         %return;
      %end;
      %else %do;
         %put WARNING:: Timed out waiting for update access to %upcase(&member)...;
         %if (&onfail eq STOP) %then %do;
            %abort cancel;
         %end;
         %else
         %if (&onfail eq SYSCC) %then %do;
            %let syscc=&SYSLCKRC;
            %return;
         %end;
      %end;
   %end;
   %else
   %if (&action eq CLEAR) %then %do;
      lock &member query;
      %if &SYSLCKRC = %sysrc(_SWLKYOU) %then %do;
         lock &member clear;
      %end;
   %end;
%mend;

%* Version 1 of SPDE locking macro ;
%macro lock_spde(member=,timeout=,retry=,action=,onFail=,unlock=,email=);
   %let action=%upcase(&action);
   %let onfail=%upcase(&onfail);

   %if (&action eq LOCK) %then %do;
      %local starttime locked timedout notes source mprint slept;
      %let starttime = %sysfunc(datetime());
      %let locked    = 1;
      %let timedout  = %sysevalf(%sysfunc(datetime()) gt (&starttime + &timeout));
      %let notes     = %sysfunc(getoption(notes));
      %let source    = %sysfunc(getoption(source));
      %let mprint    = %sysfunc(getoption(mprint));

      %* do not combine these two options statements ;
      options nomprint;
      options nosource;
      %do %while (&locked and not &timedout);
         options nonotes;
         data &member;
            modify &member;
            call symputx("locked",0);
            stop;
         run;
         options notes;
         %if (&locked) %then %do;
            %put NOTE:: Waiting for update access to %upcase(&member)...;
            %let slept=%sysfunc(sleep(&retry));
         %end;
         %let timedout = %sysevalf(%sysfunc(datetime()) gt (&starttime + &timeout));
      %end;
      options &notes;
      options &source;
      options &mprint;
      %if (&timedout) %then %do;
         %* At this point, we have timed out ;
         %* Use the %handle macro to list and close file locks, ;
         %* and try one last time to lock the member ;
         %handle(data=&member,unlock=&unlock,email=&email)
         options nomprint;
         options nosource;
         options nonotes;
         data &member;
            modify &member;
            call symputx("locked",0);
            stop;
         run;
         options &notes;
         options &source;
         options &mprint;
         %if (not &locked) %then %do;
            %put NOTE:: Update access is available for %upcase(&member)...;
            %return;
         %end;
         %else %do;
            %put WARNING:: Timed out waiting for update access to %upcase(&member)...;
            %if (%upcase(&onFail) eq STOP) %then %do;
               %abort cancel;
            %end;
            %else
            %if (%upcase(&onFail) eq SYSCC) %then %do;
               %* Since we do not have &SYSLCKRC, make up a generated return code meaning "timeout" ;
               %* 70031 seems to be the value of &SYSLCKRC when the lock statement fails on a base dataset ;
               %* so use it to be consistent with the base locking macro ;
               %let syscc=70031;
            %end;
         %end;
      %end;
      %else %do;
         %put NOTE:: Update access is available for %upcase(&member)...;
      %end;
   %end;
   %else
   %if (&action eq CLEAR) %then %do;
      %* dummy action ;
   %end;
%mend;

%* Version 2 of SPDE locking macro ;
/*
%macro lock_spde_v2(member=,timeout=,retry=,action=,onFail=,unlock=,email=);
   %let action=%upcase(&action);
   %let onfail=%upcase(&onfail);

   %if (&action eq LOCK) %then %do;
      %local dsid locked starttime timedout slept;
      %let dsid       = %sysfunc(open(&member));
      %let locked     = %eval(&dsid le 0);
      %let starttime  = %sysfunc(datetime());
      %let timedout   = %sysevalf(%sysfunc(datetime()) gt (&starttime + &timeout));
      %do %while (&locked and not &timedout);
         %put NOTE:: Waiting for update access to %upcase(&member)...;
         %let slept    = %sysfunc(sleep(&retry,1));
         %let dsid     = %sysfunc(open(&member));
         %let locked   = %eval(&dsid le 0);
         %let timedout = %sysevalf(%sysfunc(datetime()) gt (&starttime + &timeout));
      %end;
      %do dsid=&dsid %to 0 %by -1;
         %let rc = %sysfunc(close(&dsid));
      %end;
      %if (&timedout) %then %do;
         %* At this point, we have timed out ;
         %* Use the %handle macro to list and then close file locks, ;
         %* and try one last time to lock the member ;
         %handle(data=&member,unlock=&unlock,email=&email);
         %let dsid     = %sysfunc(open(&member));
         %let locked   = %eval(&dsid le 0);
         %let rc = %sysfunc(close(&dsid));
         %if (not &locked) %then %do;
            %put NOTE:: Update access is available for %upcase(&member)...;
            %return;
         %end;
         %put WARNING:: Timed out waiting for update access to %upcase(&member)...;
         %if (%upcase(&onFail) eq STOP) %then %do;
            %abort cancel;
         %end;
         %else
         %if (%upcase(&onFail) eq SYSCC) %then %do;
            %* Since we do not have &SYSLCKRC, make up a generated return code meaning "timeout" ;
            %* 70031 seems to be the value of &SYSLCKRC when the lock statement fails on a base dataset ;
            %* so use it to be consistent with the base locking macro ;
            %let syscc=70031;
         %end;
      %end;
      %else %do;
         %put NOTE:: Update access is available for %upcase(&member)...;
      %end;
   %end;
   %else
   %if (&action eq CLEAR) %then %do;
      %* dummy action ;
   %end;
%mend;
*/

%* Now check the engine type of the source dataset and call the correct utility macro ;
%let engine=%get_data_attr(&member,engine);

%* Save the current system error code up to this point ;
%let save_syscc=&syscc;

%if (&engine eq SPDE) %then %do;
   %* use the V1 version of the SPDE locking macro ;
   %lock_spde(member=&dsname,timeout=&timeout,retry=&retry,action=&action,onFail=&onFail,unlock=&unlock,email=&email)
%end;
%else %do;
   %* assume other engines (BASE, V6, V7, V9, etc) are base and support the lock statement ;
   %lock_base(member=&dsname,timeout=&timeout,retry=&retry,action=&action,onFail=&onFail,unlock=&unlock,email=&email)
%end;

%* If the %handle macro was called with unlock=yes, ;
%* and if the file handles were closed, ;
%* then reset the system error code to its previous value ;
%if (&handle_rc eq 0) %then %let syscc=&save_syscc;

%quit:
%mend;

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


text:
/*====================================================================
Program Name            : RunAll.sas
Purpose                 : Runs SAS programs asynchronously and
                          multi-threaded, honoring any job
                          dependencies.
SAS Version             : SAS 9.1.3
Input Data              : N/A
Output Data             : N/A

Macros Called           : None

Originally Written by   : Scott Bass
Date                    : 01MAY2010
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 required macro variables and programs dataset in a separate
program, then %include this program.

This program would normally be located in the base directory of the
study.

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

This program uses SAS as the "scripting engine" to submit additional
SAS programs in batch mode.  This makes this approach portable to both
Windows and Unix.

The program honors any dependencies specified in the list of programs.
Programs dependent on the completion of other programs will not run
until the "parent" program(s) finish.  This is usually relevant to SDTM
or ADAM processing, although this program can also be used to
asynchronously run independent TFL programs.

Set the macro variable abort=Y to abort processing if any upstream
program ends in error.

Set the macro variable max_threads to limit concurrent threads to that
number.  In principle, there should always be a value for max_threads,
so always set it to to some value, i.e. a number high enough to get
maximum throughput. However, you should pay attention to any decreased
machine performance when running multiple SAS processes, and adjust
max_threads accordingly.

More threads is not necessarily better.  On my dual core laptop, I get
the best elapsed time with max_threads=2.  Use the elapsed time results
at the bottom of the log to derive the best value for max_threads for
your environment.

It is a good idea to set abort=Y and run with a small number of
programs until all parameters are correct and the process is debugged.
If SAS fails to initialize (usually due to bad configuration settings),
it is difficult to break out of the main SAS driver program. Setting
abort=Y and max_threads=1 should be sufficient to abort if the SAS
configuration parameters are incorrect.

If running programs from a single directory under a programming
environment (prod or qc), you can either define -log and -print options
in an (environment specific) SAS configuration file, as parameters to
the command string (&cmd), or in the programs dataset.

If running programs from multiple directories, or across programming
environments (eg. all of SDTM, ADAM, and TFLs, or from both prod and qc
environments), then the program paths and/or -log and -print options
must be specified in the programs dataset since these values are now
dynamic rather than static.

You can specify a list of programs via the -sysparm invocation
option to limit the list of programs to run.  The program names
specified on the sysparm option must exactly match the >basenames<
as specified in the datalines statement below
(eg. without path, with extension)

A run summary report is created at the end of the job run, indicating
the run status for each program.

To debug the code generation process, comment out the line
"%include code", then review the code via "fslist code".

To prevent warning messages in the log about unable to read
sasuser.profile, create a copy of your sasuser directory, and
configure the batch runs to use the copied sasuser directory and
the -rsasuser option.

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

/* ===================================================================
Define required macro variables and programs dataset in separate program
=================================================================== */

%macro RunAll(data=programs, debug=N);

proc optsave out=options;
run;

options ls=200;

proc sort data=&data;
   by group program;
run;

%* further process programs dataset ;
data &data;
   length group 8 env $32 program $200 source $200 path $200 basename filename $32 ext $3 fullpath log print $200 taskname status $32;
   set &data;
   pos = findc(program,"&fs",-1000);
   if pos then path=substr(program,1,pos-1);
   basename = substr(program,pos+1);
   filename = scan(basename,1,".");
   ext      = scan(basename,2,".");
   if (missing(ext)) then ext="sas";
   fullpath = quote(catx("&fs",source,path,catx(".",filename,ext)));
   taskname = filename;
   status   = filename;
run;

%* delete all status macro variables from any previous runs ;
proc sql noprint;
   select status into :status_vars separated by " " from &data;
quit;

%symdel &status_vars / nowarn;

%* Note:  a (temporary) physical file results in "truncated record" on the %include statement. ;
%* A source catalog entry does not have this issue.  I tried the S2= option on the %include ;
%* statement but this did not fix the issue.  A catalog entry also does not have a lock on the ;
%* file if it is open via fslist (for example if you forget to close fslist while debugging). ;
%* So why bother, just use a catalog entry ;
filename code catalog "work.run.runall.source";

%* generate code ;
options noquotelenmax;
data _null_;
   length tasklist statuslist $1000;
   call missing(tasklist, statuslist);
   file code lrecl=1000;

   if (_n_=1) then do;
      put @1 '%global abort;';
      put @1 '%macro RunPgms;';
   end;

   do i=1 to &max_threads until (last.group);
      set &data;
      by group;
      if (^missing("&sysparm")) then
         if (^find(cats("&fs",basename,"&fs"),cats("&fs","&sysparm","&fs"),"t")) then
            continue;
      tasklist   = catx(" ",tasklist,taskname);
      statuslist = catx(" ",statuslist,status);
      put @4 '%global ' status +(-1) ";";
      put @4 "systask command " "'" "%superq(command)" @;
      if ^missing(log)   then put "-log "   log @;
      if ^missing(print) then put "-print " print @;
      put "-sysin " fullpath +(-1) @;
      put "' " taskname= status= "nowait;";
   end;

   if (^missing(tasklist)) then
      put @4 "waitfor _all_ " tasklist +(-1) ";";

   do i=1 to &max_threads until (last.group);
      set &data end=end;
      by group;
      if (^missing("&sysparm")) then
         if (^find(cats("&fs",basename,"&fs"),cats("&fs","&sysparm","&fs"),"t")) then
            continue;
      put @4 '%if (%upcase(&abort)=Y and not %index(&abort_codes,&' status +(-1) ')) %then %return;';  /* poor mans macro IN operator */
   end;

   if (end) then do;
      put '%mend;';
      put '%RunPgms;';
   end;
run;

%* Run generated code.  Run "fslist code" command to preview generated code ;
%if (&debug=N) %then %do;
   %include code / source2;

   proc format;
   value status_code
      .           = "Program was not run"
      low -< 0    = "SAS Failed to Initialize"
      0           = "SAS Ended Successfully"
      1           = "SAS Ended With Warnings"
      2           = "SAS Ended With Errors"
      3           = "User issued the ABORT statement"
      4           = "User issued the ABORT RETURN statement"
      5           = "User issued the ABORT ABEND statement"
      6           = "SAS internal error"
      >6 - high   = "User specified RETURN code"
   ;
   run;

   data status;
      format program return_code results log;
      set &data;
      if (^missing("&sysparm")) then
         if (^find(cats("&fs",basename,"&fs"),cats("&fs","&sysparm","&fs"),"t")) then
            delete;
      if (symexist(status)) then return_code = symgetn(status);
      results = put(return_code,status_code.);
      log     = cats(dequote(log),"&fs",filename,".log");
      keep program return_code results log;
   run;

   * create batch run summary report ;
   filename summrpt "&summary_rpt";
   %let rc=%sysfunc(fdelete(summrpt));

   ods _all_ close;
   %if (&summary_rpt_type eq html) %then %do;
      ods results on;
      ods &summary_rpt_type (id=summrpt) file=summrpt;
      options center;
      %let log=display;
   %end;
   %else
   %if (&summary_rpt_type eq rtf) %then %do;
      ods results off;
      ods &summary_rpt_type (id=summrpt) file=summrpt;
      options center;
      %let log=display;
   %end;
   %else
   %if (&summary_rpt_type eq txt) %then %do;
      ods listing file=summrpt;
      options nocenter;
      %let log=noprint;
   %end;

   options formchar="|----|+|---+=|-/\<>*" nonumber;
   options ls=max ps=max;

   title1 "RunAll Summary Report";
   title2 "%sysfunc(date(),date9.)";
   proc report data=status nowd box;
      column program return_code results log;
      define program       / display   width=32 "Program Name";
      define return_code   / display   width=6  "Return Code";
      define results       / display   width=40 "Results";
      define log           / &log      width=80 "Program Log";
      compute return_code;
         select (return_code);
            when (.)  call define(_row_,"style","style=[background=lightyellow]");  /* program was not run */
            when (0)  call define(_row_,"style","");
            when (1)  call define(_row_,"style","style=[background=lightorange]");
            otherwise call define(_row_,"style","style=[background=red]");
         end;
      endcomp;
      compute log;
         urlstring=cats("file://",trim(log));
         call define(_col_,"url",urlstring);
      endcomp;
   run;

   ods _all_ close;
   ods results on;
   ods listing;

   title;
   * display batch run summary report ;
   * use default application associated with the file extension ;
   systask command " ""%sysfunc(pathname(summrpt))"" " taskname="Display Summary Report" nowait cleanup;
   killtask "Display Summary Report";
   options ls=&ls ps=&ps;
%end;
%else %do;
   %* write the generated code to the log ;
   data _null_;
      infile code;
      input;
      putlog _infile_;
   run;
%end;

proc optload data=options;
run;

%mend;


text:
/*=====================================================================
Program Name            : subset_data
Purpose                 : Subset the input dataset by observations and
                          variables. Also rename variables.
SAS Version             : SAS 9.1.3
Input Data              : None
Output Data             : None

Macros Called           : parmv

Originally Written by   : Scott Bass
Date                    : 27APR2007
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:

%subset_data
(
   data=sashelp.class
   ,out=class
   ,obs=%str(1-5 or 11-15 or 20-30)
   ,rename=%str(Sex=Gender)
   ,keep=Name Age Gender
);

   Subset sashelp.class dataset,
      outputting work.class dataset
      keeping observations 1-5, 11-15 (10 observations in total)
         (there are only 19 observations in sashelp.class)
      renaming variable Sex to Gender
      keeping variables Name Age Gender

%subset_data
(
   data=sashelp.class
   ,out=class
   ,rename=%str(Sex=Gender)
   ,where=%str(Gender="F")
);

   Subset sashelp.class dataset,
      outputting work.class dataset,
      keeping all Female records

data class;
   set sashelp.class;
   if (sex="F") then call missing(of _all_);
run;

%let missing=%sysfunc(getoption(missing,keyword));
options missing=" ";

%subset_data
(
   data=class
   ,out=test
   ,if=%str(not missing(cats(of _all_)))
);

options &missing;

   Subset sashelp.class dataset,
      outputting work.class dataset,
      deleting records where all variables are missing
         (i.e. completely blank records)

This is a common problem with Excel, since it often "loses track"
of the end of data.

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

Subset data created by a PROC IMPORT of an Excel spreadsheet.

The OBS parameter is parsed, and converted to a subsetting if
statement.  There is no error if the obs parameter is greater than
the number of observations in the input dataset.

If the RENAME parameter is specified, all other parameters (i.e.
WHERE clause) should use the renamed variable name.

This is a pretty simple macro, just meant to make a common job task
take less code to implement in the calling program.

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

%macro subset_data
/*---------------------------------------------------------------------
Subset the input dataset by observations and variables.
Also rename variables.
---------------------------------------------------------------------*/
(DATA=&syslast /* Input dataset (REQ).                               */
               /* Default is &syslast (last dataset created).        */
,OUT=&syslast  /* Output dataset (REQ).                              */
               /* Default is &syslast (last dataset created).        */
,WHERE=        /* Where clause (Opt).                                */
               /* Do not specify the where keyword.                  */
,IF=           /* Subsetting If statement (Opt).                     */
               /* Do not specify the if keyword.                     */
               /* The statement must be in the form of a subsetting  */
               /* if, not a subsetting delete.                       */
,FIRSTOBS=     /* First observation to keep (Opt).                   */
,LASTOBS=      /* Last observation to keep (Opt).                    */
,OBS=          /* List of observations to keep (Opt).                */
               /* Use this if you need to keep a non-contiguous      */
               /* range of observations.  Specify the list as a      */
               /* range separated by the keyword "or".               */
               /* See examples below.                                */
,KEEP=         /* Variables to keep (Opt).                           */
               /* Should be syntactically correct variable list,     */
               /* such as A-C X Y Z, F1-F3 F5, _numeric_, etc.       */
,DROP=         /* Variables to drop (Opt).                           */
               /* Should be syntactically correct variable list,     */
               /* such as A-C X Y Z, F1-numeric-F10, etc.            */
,RENAME=       /* Variables to rename (Opt).                         */
               /* Should be syntactically correct rename statement,  */
               /* such as F1=State F2=Institution, etc.              */
);

%local macro parmerr obs id1 id2 id3 id4 id5;

%* check input parameters ;
%let macro = &sysmacroname;
%parmv(DATA,          _req=1,_words=1,_case=U)  /* words=1 allows ds options */
%parmv(OUT,           _req=1,_words=1,_case=U)
%parmv(FIRSTOBS,      _req=0,_words=0,_case=U,_val=POSITIVE)
%parmv(LASTOBS,       _req=0,_words=0,_case=U,_val=POSITIVE)

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

%* parse the obs option ;
%if (%superq(obs) ne ) %then %do;
   %let id1 = %sysfunc(prxparse(s/\s*or\s*/ OR /i));                    %* multiple spaces to single space, uppercase OR ;
   %let id2 = %sysfunc(prxparse(s/\s*-\s*/-/i));                        %* multiple spaces around dash to no space ;
   %let id3 = %sysfunc(prxparse(s/\s+(\d+)\s+/ $1-$1 /i));              %* single numbers to numeric range ;
   %let id4 = %sysfunc(prxparse(s/(\d+)-(\d+)/($1 le _N_ le $2)/i));    %* convert to subsetting if syntax ;
   %let id5 = %sysfunc(prxparse(s/###//i));
   %let obs = %sysfunc(prxchange(&id1,-1,%str(&obs ###)));              %* trailing delimiter to ensure match on id3 ;
   %let obs = %sysfunc(prxchange(&id2,-1,&obs));
   %let obs = %sysfunc(prxchange(&id3,-1,&obs));
   %let obs = %sysfunc(prxchange(&id4,-1,&obs));
   %let obs = %sysfunc(prxchange(&id5,-1,&obs));
   %let obs = if &obs;
   %syscall prxfree(id1);
   %syscall prxfree(id2);
   %syscall prxfree(id3);
   %syscall prxfree(id4);
   %syscall prxfree(id5);
%end;

data &out;
   set &data
   %if (%quote(&firstobs.&lastobs.&rename) ne ) %then %do;
      (
      %if (&firstobs ne ) %then %do;
         firstobs=&firstobs
      %end;
      %if (&lastobs ne ) %then %do;
         obs=&lastobs
      %end;
      %if (%quote(&rename) ne ) %then %do;
         rename=( &rename )
      %end;
      )
   %end;
   ;
   %if (%quote(&obs) ne ) %then %do;
      &obs;
   %end;
   %if (%quote(&where) ne ) %then %do;
      where &where;
   %end;
   %if (%quote(&if) ne ) %then %do;
      if &if;
   %end;
   %if (%quote(&keep) ne ) %then %do;
      keep &keep;
   %end;
   %if (%quote(&drop) ne ) %then %do;
      drop &drop;
   %end;
run;

%quit:
%mend;

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