Chapter 17
Stability

Backpacking 8,000 miles throughout Central and South America, you learn to take very little for granted except that change is constant.

Sleeping in so-called primera clase (first class) buses seemed cramped and unforgiving—that is, until you've spent the night on the cement floor of an actual bus terminal. However, it wasn't the occasional squalid conditions but rather the constant variability that at times grew exhausting.

While El Salvador and Ecuador use exclusively U.S. currency, the first stop in other countries was always to exchange or withdraw local money. Upon departing a country, I often had so little currency (or such small denominations) that it wasn't worth exchanging the remainder, so I began amassing wads of small bills and bags of coins that I'd squirrel away.

In many cities I was only able to spend a single night—the hostel little more than a secure place to sleep. There is the frisson of arrival, not knowing whom you'll encounter and what thrilling adventures you'll share for a night, but never a sense of stability.

Private rooms, shared rooms, dorm rooms—often a surprise until you arrive, as the act of reserving a bed sometimes carries more wishful intention than actual binding contract. Most hostels did have Wi-Fi, although often only dial-up service on which 50 guests were simultaneously trying to Skype to parts unknown.

Nearly all hostels had wooden boxes or metal gym lockers in which valuables could be securely stowed, but each time I was convinced that I'd finally purchased a lock that universally fit all lockers, I was surprised to find one that didn't. But of course, for only US$5—sometimes half the price of the room—any hostel will sell you a lock that fits their brand of lockers.

Cell phone carriers and cell service were just as diverse—Tigo in Guatemala and El Salvador, Claro in Colombia, Movistar in Peru. I was switching out SIM cards and cell phones as fast as a narcotrafficker. And because charging equipment always matched the country's power supply, it was not uncommon to see three and four converters and adapters creatively connected to charge a single phone.

When I finally arrived in Buenos Aires, having looped through Patagonia and trekked across the Pampas, I checked into a hostel and didn't leave the city for a week until my flight to the States. While I may have missed a rugged adventure or two, I was content to be confined to the Paris of South America, and the one currency, one bed, one shower, one Wi-Fi, one locker, and one cell phone were refreshing. The lack of change…was a welcome change.

images

By the time I had reached Buenos Aires, I had amassed five locks, four cell phones, several adapters and converters, two pounds of coinage, and more than 100 Wi-Fi passwords saved in my laptop. While these all had fond memories associated with them, they collectively represented 8,000 miles of variability, inefficiency, and risk.

I've since purchased a universal adapter/converter and global cell phone coverage, and I travel with an array of locks. A goal in software development should also be to seek code stability. While some variability is inevitable—like changing money at border crossings—software maintenance can often be avoided through flexible design, development, and testing.

One cost of variability is the associated inefficiency. When it's 10 PM and you're roaming the streets trying to find an open tienda (store) to purchase a lock, this isn't an efficient use of time, and it's financially inefficient when the tienda can only sell you a five-pack of locks. Software that's not stable often requires corrective and emergency maintenance at the worst possible times, derailing other development priorities and inefficiently diverting personnel.

Another cost of variability is risk—plug an unknown adapter into an unknown power source in a dark dorm room in a new country, and wait for the blue arc to light up the night. We're instinctively drawn to the familiar because it's safe and, just as I grew to trust my surroundings more in Buenos Aires during my last week of travels, software stakeholders grow to trust software that has reliably executed for some period without need for maintenance.

Because we loathe risk, we take actions to mitigate it. In hostels, you can't immediately trust the 19 BFFs with whom you're sharing quarters, so you buy a lock, stow your pack, tuck your passport between your legs, and crash for the night with one eye open. Software testing and validation also act to mitigate risk by exposing vulnerabilities in software and ensuring that software ultimately meets needs and requirements. If software is modified after testing and validation, and thus is unstable, risk returns until retesting and revalidation can occur.

DEFINING STABILITY

Stability can be defined as the degree to which software can remain static, resisting modification or maintenance while continuing to exhibit undiminishing quality. Thus, stability describes the continuing ability of software to meet needs and requirements but does not represent neglected software that is not maintained and whose relative performance and quality decrease over time. To resist preventative and corrective maintenance, stable software should be free of errors and defects. And to resist adaptive maintenance, stable software should flexibly adapt to its environment and other sources of variability to the extent possible. Somewhat paradoxically, the more flexible software can be made, the more stable it can remain over time.

Software stability is so integral to traditional software development that it's often an unspoken expectation. Where developers and users are separate and distinct, software modification requires reentry into the software development life cycle (SDLC) to test, validate, and release each software patch or update to users. Because of this level of effort, cost, and complexity, software stability is required and modifications are typically made only through planned, periodic updates. Within end-user development environments, however, software can be modified daily or hourly as desired by end-user developers because they typically have unfettered access to the underlying code. While this flexibility has certain advantages, continual modification tremendously diminishes the benefits of testing and validation within the SDLC and reduces the integrity of software.

In many ways, stability represents the gateway from software development to software operation; once software is stable, it can be automated, scheduled, and left to perform reliably without further interaction. Moreover, because stable software first requires testing to demonstrate and validate its functionality and performance, the resultant integrity encourages code reuse and repurposing in other software. This chapter demonstrates methods that can be used to implement flexibility within SAS software, including the SAS macro language, Autoexec.sas file, configuration files, and external text files and control tables, all of which can dynamically drive performance, affording users the ability to customize software operation without modification to its code.

ACHIEVING STABILITY

Stability defines resistance to change. One definition within literature references software stability as invariability in software output or results. In this sense, stability represents a facet of reliability that describes consistent functionality and thus dynamic rather than static performance. This sense of stability is described in the “Consistency” section in chapter 4, “Reliability,” but is not further discussed in this chapter. Thus, the term stability, as used throughout this text, describes static code that resists the need for modification or maintenance while continuing to meet needs and requirements.

As described in chapter 1, “Introduction,” stable software requires stable requirements, defect-free code, and adaptive flexibility. The lack of any one of these elements can result in unstable software that inefficiently requires frequent maintenance. If maintenance is not performed on unstable software, reliability can plummet as software either begins to fail or fails to meet established requirements. This unfortunate relationship too often pits reliability against stability as mutually exclusive or opposing pursuits; however, with appropriate planning, design, development, and testing, both dimensions of quality can be achieved.

STABLE REQUIREMENTS

Stable technical requirements are a luxury afforded very few software development projects, especially within environments that espouse Agile development methods and principles. Even in Waterfall development environments that favor heavy initial planning and design to militate against later design changes, requirements are often modified once software is in production, either to improve performance or deliver additional functionality. For example, a SAS analytic module might be developed to be run on one data stream but, after two months of successful performance, customers might require that it be modified to support additional data streams.

The developers might not have been able to predict this repurposing, so the requirements change doesn't represent a defect. Once the requirement is implemented, however, adaptive maintenance must repurpose the software to encompass its expanded objective. Thus, the extent to which requirements can be finalized during the design phase of the SDLC will facilitate stability in software and efficiency in the software development environment.

Software stability is sometimes conceptualized as being more closely aligned with Waterfall development methodologies that espouse big design up front (BDUF). After all, the Agile Manifesto does value “responding to change over following a plan.”1 Despite a commitment to flexibility, Agile methodologies still expect software requirements to remain stable for the duration of each iteration. For example, while the road ahead may be tortuous and evolving based on new objectives and business opportunity, developers always have a clear, stable vision of their current workload in the present iteration.

Formalizing written requirements aids stability because it memorializes the intent, objective, and technical specifications of software. Requirements provide developers not only with functional intent but also the degree of quality to which software should be developed. Moreover, once development has been completed, stakeholders can compare the software product against technical requirements and, when implemented, a formalized test plan. Without formalized requirements, software projects can suffer from scope creep in which software never seems to be complete because new functionality or performance requirements are continually added.

DEFECT-FREE CODE

Defect-free code arguably can never be achieved but, where high-performing software is required, a formalized implementation of the SDLC can improve software performance. The extent to which software can be intentionally designed, developed, and tested in a planned rather than ad hoc, frenetic fashion will always support fewer defects and errors. Software testing is key to facilitating code accuracy and, where a formalized test plan exists, to further validating software against testing requirements.

Because a formalized test plan describes testable elements that have been agreed upon by developers, customers, and possibly other stakeholders, successful test plan completion can unambiguously demonstrate software completion. But even during software planning and design, discussion about test plans and test cases can elucidate ambiguity in needs and requirements that could have led to errors in later software development. Thus, when metrics are proposed that will later be utilized to measure the success of technical specifications, customers can ensure early in the SDLC whether those requirements will be able to discriminate adequately between software success and failure.

Software upgrades and updates are common components of the SDLC operations and maintenance (O&M) phase. Users of more traditional software applications are accustomed to patches and other downloads that regularly install behind the scenes to correct defects, improve security, or provide additional performance or functionality. However, the extent to which software updates are required for corrective maintenance diminishes time that developers could have invested in other development activities. By adequately testing software and reducing corrective maintenance, greater code stability can be achieved.

DYNAMIC FLEXIBILITY

That flexible, dynamic software facilitates software stability may at first appear to be a misnomer but in fact rigid code is highly unstable. In SAS software, flexibility is often achieved through the SAS macro language, which dynamically writes Base SAS code, enabling software to be more efficiently developed. Most notably, exception handling that utilizes SAS macros can flexibly respond to changes in the environment, in data throughput, and to other shifting needs and requirements.

The implementation of flexibility through SAS macros typically occurs on a continuum. At one end, rigid software is hardcoded without flexibility in mind. For example, the following DATA step and FREQ procedure are hardcoded:

data temp;
   length char1 $10;
   char1='pupusas';
run;
proc freq data=temp;
   tables char1;
run;

Flexibility can be implemented by representing dynamic aspects of software as malleable macro variables. This middle-of-the-road approach to flexibility is common in SAS software because it enables SAS practitioners to modify functionality (and possibly performance) through the assignment of macro variables, often conveniently located in software headers or parameterized through macro invocations:

%macro doit(dsn=, var=);
data &dsn;
   length &var $10;
   &var='pupusas';
run;
proc freq data=&dsn;
   tables &var;
run;
%mend;
%doit(dsn=temp, var=char1);

For many software purposes this degree of flexibility is sufficient; however, where software must truly remain static, additional flexibility can be gained by removing parameter assignments from code and passing them externally. The following code can be saved as C:permdoit.sas and invoked through a batch job that utilizes the SYSPARM option to pass parameters for the data set name and variable name:

%let dsn=%scan(&sysparm,1,,S);
%let var=%scan(&sysparm,2,,S);
%macro doit(dsn=, var=);
data &dsn;
   length &var $10;
   &var='pupusas';
run;
proc freq data=&dsn;
   tables &var;
run;
%mend;
%doit(dsn=&dsn, var=&var);

Now, because all sources of variability occur outside the module, it can be saved in a software reuse library in this stable format. Various users and programs can call the batch job and, through dynamic invocation with the SYSPARM parameter, pass any values to represent the data set name and variable name without software modification. One rationale for this more dynamic code would be cases in which a checksum value is being generated after software testing to validate that software is not changed in the future, as demonstrated in the “Toward Software Stability” sections in chapter 11, “Security.” The assignment of macro variables through external files is further demonstrated later in this chapter in the “Custom Configuration Files” section.

Modularity

The remainder of this chapter demonstrates the role that modularity plays in supporting software stability. Modularity decomposes software into discrete chunks that can be developed and tested independently but which ultimately must unite to form a cohesive whole. Because modules are functionally discrete and perform only one or a limited number of actions, they are individually more likely to resist change. For example, if software is decomposed from a single, monolithic program into five separate modules saved as distinct programs, in many cases, maintenance can be performed on one module while leaving the others stable.

This approach dramatically increases stability because four of the five modules can remain unchanged. Additionally, though functional or unit testing would be required for the modified module, and integration testing would be necessary to ensure that module continued to function correctly with the other four modules, those unchanged modules wouldn't need to be tested individually because their integrity would be intact. This overall increased stability facilitates more rapid modification and more efficient maintenance.

STABILITY AND BEYOND

The following sections demonstrate the role that software modularity plays in supporting stability and in turn reusability and extensibility. The %DEL_VARS macro removes all character variables from a data set and simulates a typical child process that would be called from a parent:

* deletes all character variables *;
%macro del_vars(dsn= /* data set name in LIB.DSN or DSN format */);
%local vars;
%local dsid;
%local i;
%local vartype;
%local close;
%local varlist;
%let dsid=%sysfunc(open(&dsn, i));
%let vars=%sysfunc(attrn(&dsid, nvars));
%do i=1 %to &vars;
   %if %sysfunc(vartype(&dsid,&i))=N %then %do;
      %let varlist=&varlist %sysfunc(varname(&dsid,&i));
      %end;
   %end;
%let close=%sysfunc(close(&dsid));
data &dsn (keep=&varlist);
   set &dsn;
run;
%mend;

To demonstrate the %DEL_VARS macro, the following DATA step and macro invocation simulates use of %DEL_VARS in a parent process, %ETL. In the second invocation of the PRINT procedure, the character variables CHAR1 and CHAR2 have been removed from the Final data set:

data final;
   length char1 $10 char2 $10 num1 8 num2 8;
   char1='lower';
   char2='case';
   num1=5;
   num2=0;
   output;
run;
%macro etl(dsn=);
proc print data=&dsn;
run;
%del_vars(dsn=&dsn);
proc print data=&dsn;
run;
%mend;
%etl(dsn=final);

In this scenario, the %ETL macro is intended to simulate more extensive production software that has been tested, validated, automated through batch processing, and scheduled to run recurrently. When the %ETL macro is executed, the Final data set is modified so that only numeric variables are retained. These capabilities are discussed and expanded in the following sections.

Stability Promoting Reusability

Because the %DEL_VARS macro in the “Stability and Beyond” section has been tested and validated, it could be removed from the body of the %ETL program and saved to a shared location so that all SAS practitioners within a team or organization can access and utilize it. SAS software can later reference the %DEL_VARS macro with the SAS Autocall Macro Facility or with the %INCLUDE statement.

If SAS practitioners later needed to create a quantitative analysis process that relied on only numeric variables, they could implement the %DEL_VARS macro to reduce input/output (I/O) resources wasted on processing unneeded character variables. Because %DEL_VARS provides this functionality and represents stable, tested code, %DEL_VARS could be implemented with ease. The following code simulates the analytical process with the MEANS procedure.

data final;
   length char1 $10 char2 $10 num1 8 num2 8;
   char1='lower';
   char2='case';
   num1=5;
   num2=0;
   output;
run;
%macro analysis(dsn=);
%del_vars(dsn=&dsn);
proc means data=&dsn;
run;
%mend;
%analysis(dsn=final);

Note that in this example, the child process %DEL_VARS did not need to be modified, so no unit testing was required, only integration testing to determine how the macro interacts with the parent process. Like the child process %DEL_VARS, SAS macros and modules will be more likely to be reused when they represent independent, modular, tested, stable components.

Stability Promoting Extensibility

Continuing the scenario depicted in the “Stability Promoting Reusability” section, SAS practitioners have been successfully implementing the %DEL_VARS module in various software products for a few months when a developer notes that its functionality could be expanded to retain only numeric variables as well. This is a common example of software extension in which added functionality is desired and, rather than creating new software from scratch, existing code is expanded.

Whenever possible, it's important to maintain backward compatibility to current uses of the module, so design and testing should attempt to leave the default functionality unchanged. In this case, the module should still be able to remove all character variables from a data set. Software extensibility speaks to repurposing software or building something new from something old. Where extensibility can also incorporate backward compatibility, however, this benefits software development because new functionality can be added while maintaining a single module or code base.

Extensibility and backward compatibility are achieved and demonstrated in the upgraded %DEL_VARS macro:

* deletes either all character or all numeric variables *;
%macro del_vars(dsn= /* data set name in LIB.DSN or DSN format */,
   type=C /* C to drop character vars, N to drop numeric */);
%local vars;
%local dsid;
%local i;
%local vartype;
%local close;
%local varlist;
%let dsid=%sysfunc(open(&dsn, i));
%let vars=%sysfunc(attrn(&dsid, nvars));
%do i=1 %to &vars;
   %if %sysfunc(vartype(&dsid,&i))=%upcase(&type) %then %do;
      %let varlist=&varlist %sysfunc(varname(&dsid,&i));
      %end;
   %end;
%let close=%sysfunc(close(&dsid));
data &dsn (drop=&varlist);
   set &dsn;
run;
%mend;
data final;
   length char1 $10 char2 $10 num1 8 num2 8;
   char1='lower';
   char2='case';
   num1=5;
   num2=0;
   output;
run;
%macro etl(dsn=);
proc print data=&dsn;
run;
%del_vars(dsn=&dsn, type=N);
proc print data=&dsn;
run;
%mend;
%etl(dsn=final);

The new %DEL_VARS macro is now overloaded, in that it additionally accepts the optional TYPE parameter—denoted by C or N—to drop only character or numeric variables. Thus, new invocations of %DEL_VARS can take advantage of the additional functionality of the TYPE parameter, while existent invocations that don't include TYPE—such as the example demonstrated in the “Stability Promoting Reusability” section—will default to deleting only character variables when TYPE is absent, thus maintaining past functionality. Overloading is demonstrated in the “Regression Testing” section in chapter 16, “Testability,” and often facilitates backward compatibility in macros that increase in scope over time, thus promoting software stability.

If the %DEL_VARS macro functionality could not have been extended successfully while ensuring backward compatibility (as was accomplished in this scenario), an important decision would have had to have been made. The original version of the module could have remained intact, thus supporting all current uses in various programs, while a new macro could have been created as a new module to meet the new functional requirement. Or, the macro still could have been modified to meet the new functional requirement, but this would have invalidated its other current uses.

Thus, where backward compatibility cannot be achieved, developers must essentially choose between code stability and maintainability. They can modify two separate (but similar) code bases, which incurs future inefficiency every time maintenance is required, or they can modify all current programs that reference the changed module, albeit being able to maintain a single software module going forward. Each method will be warranted in certain circumstances so the costs and benefits must be weighed.

Stability Promoting Longevity

Software longevity is introduced in the “Longevity” section in chapter 4, “Reliability,” and even the most meager software requirements should state an expected software lifespan. Intended lifespan enables developers to understand whether they are designing some ephemeral, fly-by-night software to solve a tactical problem or building an enduring, strategic solution. When software is intended to be enduring, the degree to which software can be maintained with minimal modifications can be facilitated with stable, reusable code.

Continuing the scenario from the “Stability Promoting Extensibility” section, the revised %DEL_VARS module has now been reused multiple times within ETL software, and its functionality has been extended so that either character or numeric variables can be retained. Because the module is stable and has gained significant popularity, it is more likely to be appropriately maintained because effort invested in its maintenance benefits numerous dependent software products or projects. If developers instead had built a new macro (to delete only numeric variables), this macro in addition to %DEL_VARS would have had to have been separately maintained, thus effectively decreasing the value of each. Because maintenance is critical to enduring software, the extent to which new SAS software can be built using existing, tested, trusted, stable modules will facilitate software longevity of the modules being reused.

Stability Promoting Efficiency

Another benefit of stable code is that when advancements are made to a software module, those changes can made in one location yet benefit all programs that call the module. For example, the %DEL_VARS macro as defined is inefficient because regardless of whether any variables are removed, the data set still is read and rewritten, requiring I/O resources. But what occurs if the code attempts to eliminate character variables when none exist? The module unnecessarily recreates the original data set, retaining all variables.

A more efficient solution would instead identify similar cases in which all variables were being maintained—because the original data sets were either all numeric or all character—and skip the gratuitous DATA step. This logic is conveyed in a subsequent version of %DEL_VARS:

* deletes either all character or all numeric variables *;
%macro del_vars(dsn= /* data set name in LIB.DSN or DSN format */,
   type=C /* C to drop character vars, N to drop numeric */);
%local vars;
%local dsid;
%local i;
%local vartype;
%local close;
%local varlist;
%local empty;
%let empty=YES;
%let dsid=%sysfunc(open(&dsn, i));
%let vars=%sysfunc(attrn(&dsid, nvars));
%do i=1 %to &vars;
   %if %sysfunc(vartype(&dsid,&i))=%upcase(&type) %then %do;
      %let varlist=&varlist %sysfunc(varname(&dsid,&i));
      %let empty=NO;
      %end;
   %end;
%let close=%sysfunc(close(&dsid));
%if &empty=NO %then %do;
   data &dsn (drop=&varlist);
      set &dsn;
   run;
   %end;
%mend;
data final;
   length char1 $10 char2 $10 num1 8 num2 8;
   char1='lower';
   char2='case';
   num1=5;
   num2=0;
   output;
run;
%macro etl(dsn=);
proc print data=&dsn;
run;
%del_vars(dsn=&dsn, type=C);
proc print data=&dsn;
run;
%mend;
%etl(dsn=final);

Had the module not been stable and tested, the decision to correct the inefficiency through perfective maintenance might not have been prioritized; however, because the module was reused in this and other software, the decision to maintain the module and make it more efficient was more readily supported. Stable, reusable code will generally be more valued (and thus more likely to be refactored) because its value is construed not only from one software product but also from multiple programs that implement it.

But one more glaring inefficiency exists in the software, which results from violation of modular software design. Because the %DEL_VARS macro not only generates the space-delimited list of variables to remove (&VARLIST) but also implements the DATA step, it is neither functionally discrete nor loosely coupled. The DATA step inside the %DEL_VARS macro is gratuitous because &VARLIST could be implemented directly within the MEANS procedure. This inefficiency is demonstrated in the “Stability Promoting Reusability” section:

%macro analysis(dsn=);
%del_vars(dsn=&dsn);
proc means data=&dsn;
run;
%mend;

To remedy this inefficiency, while ensuring that the code remains backward compatible, the following %DEL_VARS macro requires three modifications. First, the DEL parameter should be added to the macro definition to demonstrate that the macro can optionally generate only the &VARLIST macro variable without deleting those variables within the gratuitous DATA step. The second change requires that &VARLIST be changed from a local to a global macro variable to facilitate access by the parent process. And third, the conditional logic that evaluates whether to execute the gratuitous DATA step should examine the value of the DEL parameter. These changes are finalized in the following code:

* deletes either all character or all numeric variables *;
%macro del_vars(dsn= /* data set name in LIB.DSN or DSN format */,
   type=C /* C to drop character vars, N to drop numeric */,
   del=Y /* Y to delete from data set, N to generate VARLIST only */);
%local vars;
%local dsid;
%local i;
%local vartype;
%local close;
%global varlist;
%local empty;
%let empty=YES;
%let dsid=%sysfunc(open(&dsn, i));
%let vars=%sysfunc(attrn(&dsid, nvars));
%do i=1 %to &vars;
   %if %sysfunc(vartype(&dsid,&i))=%upcase(&type) %then %do;
      %let varlist=&varlist %sysfunc(varname(&dsid,&i));
         %let empty=NO;
      %end;
   %end;
%let close=%sysfunc(close(&dsid));
%if &empty=NO and &del=Y %then %do;
   data &dsn (drop=&varlist);
      set &dsn;
   run;
   %end;
%mend;

Executing the following statements now again removes all character variables from the second invocation of PRINT. But because the DROP statement occurs within the PRINT procedure rather than in the %DEL_VARS macro, this demonstrates discrete functionality while avoiding the gratuitous DATA step in newer programs that reference this macro:

data final;
   length char1 $10 char2 $10 num1 8 num2 8;
   char1='lower';
   char2='case';
   num1=5;
   num2=0;
   output;
run;
%macro etl(dsn=);
proc print data=&dsn;
run;
%del_vars(dsn=&dsn, type=C, del=N);
proc print data=&dsn (drop=&varlist);
run;
%mend;
%etl(dsn=final);

Through overloading, the revised %DEL_VARS macro is now more efficient but is also backward compatible to two former manifestations—the original usage with only the DSN parameter and the second usage with DSN and TYPE parameters. This can be an effective way to build software while supporting stability; however, original modular design should be incorporated for this type of extensibility to be effective. Because the gratuitous DATA step was trapped in the original %DEL_VARS macro, it remains as long as backward compatibility to the original intent of this macro is maintained. While stability should be an objective, the quest for stability should never outweigh common sense. Thus, when truly archaic, inefficient, or insecure former manifestations of software modules exist, their deprecation is often favored in lieu of continuing to support awkward or endless backward compatibility.

Stability Promoting Security

The “I” in the CIA information security triad (confidentiality, integrity, and availability) demonstrates the importance of integrity. Information should be accurate, and where possible, a data pedigree should be maintained that can trace data to its origin, much like the forensic documentation of crime scene evidence that maintains chain of custody. Data integrity is a common objective in data analytic development because business value is derived from data products, thus their accuracy is critical.

However, because information describes not only data but also the software that undergirds, manipulates, and analyzes it, information security policies should also be considered for software. In applications development, security policies help to ensure that software is not maliciously modified, such as through computer viruses, code injections, or other attacks. Because data analytic software products are more likely to be used internally, their attack surfaces are significantly reduced. In other words, most SAS practitioners don't write software with the forethought that it will be intentionally attacked. They instead rely on systems security, authentication, physical controls, and other methods to defend against malicious threats.

Even in data analytic software, however, a realistic threat to software integrity exists, not from malice, but from unintentional code modification or from intentional code modification that causes unintended ill effects. Software versioning exists in part for this reason—to ensure that the correct copy of software is being modified, tested, integrated, and executed. When a new version is created, past versions are archived and should remain stable unless they are required for recovery purposes. Archival stability is critical because it timestamps and provides a historical record of software. For example, if developers wanted to turn back time and run software with the functionality and performance that it exhibited two months prior, the versioned software—if archived with the necessary data and other files—would provide the integrity and stability to accomplish this.

In many environments that utilize software versioning, the integrity of the current software version is conveyed through a brief examination of the code or a timestamp representing when it was last saved. Far greater integrity can be achieved, however, by generating checksums on stable SAS program files after software testing and validation. Checksums utilize cryptographic hash functions to perform bit-level analysis of SAS programs (or any files) to create a hexadecimal string that uniquely represents each file. By obtaining a checksum on software before it executes and by validating this checksum against its baseline value, stakeholders can be assured that the software executing is secure and has not been modified since testing and validation. This technique is demonstrated in the “Checksums” section in chapter 11, “Security.”

MODULARIZING MORE THAN MACROS

While modularity is a hallmark of stable software, the examples thus far have demonstrated using modules only within a functional sense. Thus, a parent process calls a child process—a SAS macro—to perform some function after which program flow returns to the parent. While this is a common use of modular design that supports stability, it only scratches the surface of ways in which modularity can facilitate stability.

For example, the following sections simulate an ETL process that ingests, cleans, transforms, and displays data. The initial code will be improved upon throughout the next several sections to demonstrate ways in which modularity and flexibility can be infused into software to remove dynamic elements from code and thereby increase software stability. To improve readability, no exception handling is demonstrated; however, in robust production software, numerous vulnerabilities would need to be rectified through an exception handling framework.

The following three sections of code configure options, load data, and parse those data, and are saved as C:permcandy.sas. In the first section of code, SAS system options are defined and two SAS libraries are initialized, PERM and OUTPUT. If an option needs to be modified or added, however, the software must be changed. Furthermore, if the logical location of either library needs to be modified—for example, from C:perm to C:permenant—the software must also be changed. More dynamic initialization methods are demonstrated in the later “Autoexec.sas” section:

options dlcreatedir nosymbolgen nomprint nomlogic;
libname perm 'c:perm';
%let outputloc=c:permoutput;
libname output "&outputloc";

The next module simulates data ingestion in ETL software and creates the PERM.Raw_candy data set. Intentional spelling variations of the CANDY values are included:

data perm.raw_candy;
   infile datalines delimiter=',';
   length candy $20;
   input candy $;
   format candy $20.;
   datalines;
Almond Joy
Almond Joys
5th Avenue
Fifth avenue
Reese's
Reeses
Reeces
KitKat
Kitkat
Kit Kat
Snickers
Twixx
Twix
;

The third module, %CANDY_CLEANER, simulates a quality control step that cleans and standardizes data:

* ingests candy data from data set;
%macro candy_cleaner(dsn= /* data set in LIB or LIB.DSN format */);
data perm.clean_candy;
   length clean_candy $20;
   set &dsn;
   * standardize spelling variations;
   if strip(lowcase(candy)) in ("almond joy","almond joys") then    clean_candy="Almond Joy";
   else if strip(lowcase(candy)) in ("5th avenue","fifth avenue") then    clean_candy="5th Avenue";
   else if strip(lowcase(candy)) in ("reese's","reeses","reeces") then    clean_candy="Reese's";
   else if strip(lowcase(candy)) in ("kitkat","kit kat") then    clean_candy="KitKat";
   else if strip(lowcase(candy)) in ("snickers") then    clean_candy="Snickers";
   else if strip(lowcase(candy)) in ("twix","twixx") then    clean_candy="Twix";
   else clean_candy="UNK";
run;
%mend;

It's obvious that with more diverse or voluminous data, this quality control method could grow untenably large, complex, and error-prone. More importantly, however, as new values are encountered in the data and must be binned into the correct, standardized format, the code must be modified each time. Even if a more flexible quality control method (such as PERL regular expressions) were implemented within SAS, those expressions would still need to be maintained in the software itself. Because of these limitations, this module is made more dynamic in the “Business Logic” section later in this chapter.

The FORMAT procedure creates a format that categorizes the candy based on primary ingredients, but it's clear that the raw values are statically tied to the previous quality control conditional logic, so changes in that logic likely would cause the FORMAT procedure to fail outright or produce invalid results. And because the FORMAT procedure is hardcoded in the software itself, any modifications to the format logic require software modification. For example, if an analyst wanted to classify candy by texture rather than ingredients, how would this be accomplished? Moreover, what if two competing candy models were required to be maintained at the same time to satisfy the needs of distinct stakeholders? This functionality is made significantly more flexible later in the “SAS Formatting” section:

proc format;
   value $ candy_class
   "Almond Joy" = "chocolate"
   "5th Avenue" = "chocolate and caramel"
   "Reese's" = "chocolate and peanut butter"
   "KitKat" = "chocolate"
   "Snickers" = "chocolate and caramel"
   "Twix" = "chocolate and caramel";
run;

The %CANDY_DISPLAY module organizes candy by its classification and name and displays the quantity of candy by name in an HTML report. To highlight the most commonly occurring candy, candies having a frequency greater than two are highlighted in red through stoplight reporting within the REPORT procedure. But what if an analyst wanted to use a different color for highlighting, to add a title, or to save the report to a different location? Each of these customizations would require software modification:

* creates an HTML report that organizes and counts candy by classification;
%macro candy_display(dsn= /* data set in LIB or LIB.DSN format */);
ods listing close;
ods noproctitle;
ods html path="&outputloc" file="candy.htm" style=sketch;
proc report data=&dsn style(report) = [foreground=black backgroundcolor=black] style(header) = [font_size=2 backgroundcolor=white foreground=black];
   title;
   column clean_candy=candy_class clean_candy n dummy;
   define candy_class / group "Classification" format=$candy_class.;
   define clean_candy / group "Candy";
   define n / "Count";
   define dummy / computed noprint "";
   compute dummy;
      if _c3_>2 then call define("_c3_","style",       "style=[backgroundcolor=light red]");
      endcomp;
run;
ods html close;
ods listing;
%mend;
%candy_cleaner(dsn=perm.raw_candy);
%candy_display(dsn=perm.clean_candy);

As demonstrated, myriad subtle customizations would require software to be updated, and in some instances, competing customizations would inefficiently require multiple instances of the software to be maintained. Thus, by predicting ways in which software may need to be customized while in operation, developers can code dynamism and flexibility into SAS software. The following sections demonstrate incremental changes to this code that further modularize this single program into multiple files that can better support software stability. While maintenance or customization still will require modifications, those changes will be made only to ancillary files rather than the core SAS software itself, which should remain stable and secure to the extent possible.

Autoexec.sas

The Autoexec.sas file is an optional file that can be utilized to initialize a SAS session, specifying system options or executing SAS code before any programs execute. Autoexec.sas is described in detail in the SAS 9.4 Companion for Windows, and is introduced here to demonstrate its importance in stabilizing software.2 In SAS Display Manager for Windows, the default location for Autoexec.sas is C:Program FilesSASHomeSASFoundation9.4, but the default can vary by environment, SAS interface, and server configuration. In many environments, this directory will be restricted to specific users, so the SAS application also searches other locations for the Autoexec.sas file. The AUTOEXEC option can be invoked from the command prompt to start a batch session and specify the location of a specific Autoexec.sas file, thus overcoming this restriction.

To demonstrate this technique, the following customized Autoexec.sas file can be saved to C:permautoexec.sas:

libname perm 'c:perm';
options dlcreatedir nosymbolgen nomprint nomlogic;

The primary Autoexec.sas file utilized by SAS is unchanged; however, when a batch invocation specifies the AUTOEXEC option and references C:permautoexec.sas, the new file is invoked rather than the standard SAS Autoexec.sas file, ensuring that necessary libraries are assigned and system options initialized. SAS system options that cannot be modified during an active SAS session can be set within the Autoexec.sas file.

The SAS program Candy.sas now no longer requires the LIBNAME or OPTIONS statements because they appear in the Autoexec.sas file. As a golden rule, customization or initialization meant to be applied to all SAS software within an environment can be done in the primary Autoexec.sas file, while options specific to one program should be done via a separate Autoexec.sas file or configuration file, as demonstrated in the following batch invocation:

“c:program filessashomesasfoundation9.4sas.exe” -sysin c:permcandy.sas -autoexec c:permautoexec.sas -noterminal -nosplash -log c:permcandy.log -dlcreatedir

Removing the LIBNAME statement from the software and placing it in Autoexec.sas allows the code to be more flexible in the event that the logical location changes. For example, the PERM library might reference a SAS server IP address, and if this address is changed, it can be modified in a single location within Autoexec.sas, allowing all SAS software to remain stable while still referencing the new location. In environments where metadata are enforced, it is always a best practice to define SAS libraries permanently within metadata structures so they are available for use in all software.

Business Logic

The standardization of data is a common theme in SAS data analytic development, for example, where quality control techniques eliminate abbreviations, acronyms, spelling errors, and other variations that can occur in categorical data. Too often, however, this is accomplished through unmaintainable, hardcoded logic that requires modification each time the data model must be updated.

An objective in modular software design should be to remove business rules (or data models) from the software so that changes to one don't necessitate changes to the other. In addition to cleaning and standardizing values, this logic can additionally include organization of values into a matrix or hierarchy. To remove logic from software, it should be operationalized into a structure that can be parsed, often a SAS data set, Excel spreadsheet, XML file, or plain text file. The following comma-delimited text file, saved to C:permcandy_cleaning.txt, lists the standardized value in the first position with additional spelling variations following to the right. Text files are straightforward and arguably the most easily maintained representations of simple data models:

Almond Joy,almond joy,almond joys
5th Avenue,5th avenue,fifth avenue
Reese's,reese's,reeses,reeces
KitKat,kitkat,kit kat
Snickers,snickers
Twix,twix,twixx

The %CANDY_CLEANER macro next utilizes I/O functions to open the text file within the DATA step and place spelling variations into an array VARS which is compared to actual values found in the Raw_candy data set. This code could have been operationalized in numerous ways, including %SYSFUNC calls to I/O functions or a multidimensional array to capture all variations for all candy types:

* ingests candy data from data set;
%macro candy_cleaner(dsn= /* data set in LIB or LIB.DSN format */);
filename ref "c:permcandy_cleaning.txt";
data perm.clean_candy (keep=candy clean_candy);
   length clean_candy $20;
   set &dsn;
   * standardize spelling variations;
   length var $100 var2 $30;
   f=fopen("ref");
   rc1=fsep(f,",");
   do while(fread(f)=0);
      rc3=fget(f,var);
      array vars{5} $30 _temporary_;
      do i=1 to dim(vars);
         vars{i}="";
         end;
      i=0;
      do while(fget(f,var2)=0);
         i=i+1;
         vars{i}=var2;
         end;
      if strip(lowcase(candy)) in vars then clean_candy=var;
      end;
   if missing(clean_candy) then clean_candy="UNK";
run;
%mend;
%candy_cleaner(dsn=perm.raw_candy);

One advantage of using strictly I/O functions to read external data models is that because values are never converted to macro variables, they can include special characters and complex quoting without having to specially handle these values in SAS. And, while IF–THEN conditional logic has been utilized to improve code readability, software performance could likely be gained through the FORMAT procedure or a hash object lookup table.

This pursuit of excellence is exactly what underscores the importance separating business logic from the software engine itself. SAS practitioners desiring to refactor this software and implement a more efficient hash object lookup to clean and standardize data could do so by modifying only the %CANDY_CLEANER module, without ever touching the data model, thus promoting its stability and security. On the other hand, analysts who need to modify only the data model and its logic but do not want to modify the software could do so without the fear of accidentally modifying %CANDY_CLEANER. The two elements are separate and distinct.

Moreover, if analysts maintain different or competing data models, an endless number of models can be swapped in and out of the software simply by changing the SAS FILENAME reference. Thus, an even more modular and flexible solution would require the data model (currently hardcoded as C:permcandy_cleaning.txt in the software) to be passed as a parameter so that data models could be changed without touching the code. Especially within empirical and research environments in which data models are expected to be agile, this modular method supports this flexibility objective without compromising the integrity of the underlying software. In differentiated teams in which developers specialize in software and analysts focus on data analysis, this design model allows developers to separately maintain software while analysts separately maintain data models, thus facilitating the stability and security of each.

SAS Formatting

SAS formats represent a common method to clean and standardize data values. The following FORMAT procedure can largely be copied from the original business rules within the %CANDY_CLEANER module. When it is saved to a separate SAS program and referenced from %CANDY_CLEANER with the %INCLUDE statement, the formatting also represents a modular solution that can drive business rules from beyond the code itself:

proc format;
   value $ clean_candy
   "almond joy","almond joys" = "Almond Joy"
   "5th avenue","fifth avenue" = "5th Avenue"
   "reese's","reeses","reeces" = "Reese's"
   "kitkat","kit kat" = "KitKat"
   "snickers" = "Snickers"
   "twix","twixx" = "Twix"
   other = "UNK";
run;

Because of the necessary quotations, the maintenance of formats is slightly higher than that of raw text files but, as demonstrated later, the implementation can be dramatically more straightforward. The following macro represents an alternative %CANDY_CLEANER module to the one presented earlier in the “Business Logic” section. Note that when formatting is intended to permanently clean or correct data (as opposed to changing its format temporarily for display purposes), a separate variable should often be created to ensure that the format is not accidentally later removed or modified. This also facilitates comparison between the original (unformatted) and transformed (formatted) variables:

%macro candy_cleaner(dsn= /* data set in LIB or LIB.DSN format */);
data perm.clean_candy;
   set &dsn;
   length clean_candy $30;
   clean_candy=put(lowcase(candy),$clean_candy.);
run;
%mend;

In other cases, formatting is intended to change the representation of data only temporarily for display. For this purpose, the FORMAT function can be applied to the variables directly in REPORT, PRINT, MEANS, FREQ, or other procedures, only modifying the variable display for the duration of the procedure.

The CNTLIN option in the FORMAT procedure allows a control table (i.e., a SAS data set) to be used to populate a SAS format but, unfortunately, is quite restrictive in its implementation. For example, the CNTLIN option cannot accommodate a format such as CANDY_CLASS that aims to group multiple character strings into a single value. To remedy this, a modular, reusable solution can be implemented that transforms comma-delimited text files into SAS formats. This is similar to the methodology demonstrated in the “Business Logic” section but, rather than dynamically creating conditional logic, it dynamically creates a SAS format.

To begin, the business logic is saved to the comma-delimited text file C:permformat_candy_ordering.txt. As previously, the first item represents the categorical value to which subsequent values will be transformed. Thus, when Almond Joy or KitKat appears in the data, it will be classified as chocolate in the HTML report when the CANDY_CLASS format is invoked:

chocolate, Almond Joy, KitKat
chocolate and caramel, 5th Avenue, Snickers, Twix
chocolate and peanut butter, Reese's

Now, rather than hardcoding the REPORT procedure as before, this is done dynamically in a reusable macro, %INGEST_FORMAT, that requires parameters for the control table and the format name:

* creates a SAS format that categorizes or bins character data;
%macro ingest_format(fil= /* location of comma-delimited format file */,
   form= /* SAS character format being created */);
filename zzzzzzzz "&fil";
data ingest_format (keep=FMTNAME START LABEL);
   length FMTNAME $32 START $50 LABEL $50;
   fmtname="&form";
   f=fopen("zzzzzzzz");
   rc1=fsep(f,",");
   do while(fread(f)=0);
      rc3=fget(f,label);
      label=strip(label);
      do while(fget(f,start)=0);
         start=strip(start);
         output;
         end;
      end;
run;
%mend;

The CANDY_CLASS format that is created is identical to the one created earlier in the “Modularizing More Than Macros” section, with the sole exception that the OTHER option does not specify a default value when the base value cannot be found in the format. If the OTHER option is desired, this can be added with one additional line in the Format_candy_ordering.txt file and logic to parse this to create an OTHER option in the %INGEST_FORMAT macro. Each of these two different methodologies removes business rules and data models from SAS code, thus facilitating stability of SAS production software. In the next section, additional customization will be delivered through a configuration file.

Custom Configuration Files

Custom configuration files are one of the strongest advocates to flexible code that facilitates software stability. For example, in the original Candy.sas program, the location for the HTML report is hardcoded; to change this, modification of the code is necessary. To instead assign this library and logical location dynamically, the &OUTPUTLOC macro variable should be assigned in the configuration file. Thus, the &OUTPUTLOC assignment is removed from the code and the LIBNAME statement now stands alone:

libname output "&outputloc";

A second hardcoded element is the report name. Because the report is always saved as Candy.htm, each software execution overwrites previous reports. In the future, SAS practitioners might instead want to incrementally name these reports so newer reports don't overwrite older ones, or separate analysts might want to run separate copies of the software concurrently. Thus, the second modification is to assign the report file name dynamically through the software invocation:

ods html path="&outputloc" file="&outputfile" style=sketch;

A third hardcoded attribute that could benefit from flexibility is the stoplight threshold and the stoplight color inside the COMPUTE statement of the REPORT procedure. In making these elements dynamic, analysts will be able to subtly modify the style and intent of the report without having to modify any of the underlying SAS code—just the associated configuration file:

if _c3_>&thresh then call define("_c3_","style","style=[backgroundcolor=&highlight]");

To represent these customizations, the text configuration file C:permconfig_candy.txt is created:

This configuration file is required to execute the candy.sas program.
Last updated 12-21 by TMH
Note that the OUTPUT_FILE should NOT include the file path.
<OUTPUT_LIBRARY>
c:permoutput
<OUTPUT_FILE>
candy_output.html
<REPORT_STYLE>
thresh: 2
highlight: light brown

Three configuration categories (e.g., <OUTPUT_LIBRARY>) are specified, depicted in uppercase, and bracketed. Subsumed under each category are either raw data to be transferred to a macro variable or tokenized values to be further parsed. For example, under <REPORT_STYLE>, the values for &THRESH and &HIGHLIGHT are included and can be read in any order.

The configuration file is read via the %READ_CONFIG macro, which parses the data, initializes the OUTPUT library, and assigns global macro variables &OUTPUTLOC, &THRESH, and &HIGHLIGHT to be used in Candy.sas. The %READ_CONFIG macro should be launched initially in the software, just as the SAS application initially reads its own configuration file at startup:

* reads a configuration file necessary for Candy.sas execution;
%macro read_config(fil= /* logical location of configuration file */);
%global outputloc;
%let outputloc=;
%global outputfile;
%let outputfile=;
%global thresh;
%let thresh=;
%global highlight;
%let highlight=;
data _null_;
   length tab $100 category $8;
   infile "&fil" truncover;
   input tab $100.;
   if strip(upcase(tab))="<OUTPUT_LIBRARY>" then category="lib";
   else if strip(upcase(tab))="<OUTPUT_FILE>" then category="fil";
   else if upcase(tab)="<REPORT_STYLE>" then category="rpt";
   else do;
      if category="lib" then call execute('libname output         "' || strip(tab) || '";'),
      else if category="fil" then call symput("outputfile",strip(tab));
      else if category="rpt" then do;
         if strip(lowcase(scan(tab,1,":")))="thresh" then call            symput("thresh",strip(scan(tab,2,":")));
         if strip(lowcase(scan(tab,1,":")))="highlight" then call            symput("highlight",strip(scan(tab,2,":")));
         end;
      end;
   retain category;
run;
%mend;
%read_config(fil=c:permconfig_candy.txt);

The previous demonstration intentionally contains no exception handling to promote readability. However, in actual configuration files, exception handling is critical to capture input errors that may have been made when creating or modifying the file. One substantial benefit of configuration files is the simplicity with which they can be built, understood, and modified. However, even with appropriate documentation inside configuration files, their strength is also their weakness, due to the vulnerability of improper modification that can cause corresponding software to crash.

For example, if the <REPORT_STYLE> header were omitted, or either the THRESH or HIGHLIGHT parameters were omitted, these parameters would not be read, causing null values to be assigned to their respective global macro variables. To overcome this vulnerability, the following code can be inserted immediately before the %MEND statement, thus testing the length of the macros &THRESH and &HIGHLIGHT and assigning default values if the macros were not assigned:

%if %length(&thresh)=0 %then %let thresh=2;
%if %length(&highlight)=0 %then %let highlight=light red;

While overcoming these two vulnerabilities, multiple other vulnerabilities exist that would need to be mitigated in production software. For example, what if the value for the output location does not exist? What if a character value rather than numeric is given for the THRESH parameter? What if the HIGHLIGHT parameter includes a non-color value? And, finally, what happens when special characters are detected within a parameter?

Once these vulnerabilities are overcome, however, the software will be more robust and stable while enabling modification through the configuration file alone. With this added stability, both flexibility and security are increased: analysts can make minor, stylistic changes to data products without ever having to open or modify the production software.

Putting It All Together

While the resultant code is longer, more complex, and less readable, it offers flexibility that enables SAS practitioners to customize execution and output without modifying their software. In environments that require stable code—for example, those which have authoritarian change control policies, formalized testing, and validation—removing dynamic elements from software and extrapolating them to external Autoexec.sas files, configuration files, control tables, and data models will facilitate software stability.

The following code represents the updated software which also requires the external files C:permautoexec.sas, C:permconfig_candy.txt, C:permcandy_cleaning.txt, and C:permformat_candy_ordering.txt:

%include "c:permautoexec.sas";
data perm.raw_candy;
   infile datalines delimiter=',';
   length candy $20;
   input candy $;
   format candy $20.;
   datalines;
Almond Joy
Almond Joys
5th Avenue
Fifth avenue
Reese's
Reeses
Reeces
KitKat
Kitkat
Kit Kat
Snickers
Twixx
Twix
;
* reads a configuration file necessary for Candy.sas execution;
%macro read_config(fil= /* logical location of configuration file */);
%global outputloc;
%let outputloc=;
%global outputfile;
%let outputfile=;
%global thresh;
%let thresh=;
%global highlight;
%let highlight=;
data _null_;
   length tab $100 category $8;
   infile "&fil" truncover;
   input tab $100.;
   if strip(upcase(tab))="<OUTPUT_LIBRARY>" then category="lib";
   else if strip(upcase(tab))="<OUTPUT_FILE>" then category="fil";
   else if upcase(tab)="<REPORT_STYLE>" then category="rpt";
   else do;
      if category="lib" then call execute('libname output         "' || strip(tab) || '";'),
      else if category="fil" then call symput("outputfile",strip(tab));
      else if category="rpt" then do;
         if strip(lowcase(scan(tab,1,":")))="thresh" then call            symput("thresh",strip(scan(tab,2,":")));
         if strip(lowcase(scan(tab,1,":")))="highlight" then call            symput("highlight",strip(scan(tab,2,":")));
         end;
      end;
   retain category;
run;
%if %length(&thresh)=0 %then %let thresh=2;
%if %length(&highlight)=0 %then %let highlight=light red;
%mend;
* ingests candy data from data set;
%macro candy_cleaner(dsn= /* data set in LIB or LIB.DSN format */);
filename ref "c:permcandy_cleaning.txt";
data perm.clean_candy (keep=candy clean_candy);
   length clean_candy $20;
   set &dsn;
   * standardize spelling variations;
   length var $100 var2 $30;
   f=fopen("ref");
   rc1=fsep(f,",");
   do while(fread(f)=0);
      rc3=fget(f,var);
      array vars{5} $30 _temporary_;
      do i=1 to dim(vars);
         vars{i}="";
         end;
      i=0;
      do while(fget(f,var2)=0);
         i=i+1;
         vars{i}=var2;
         end;
      if strip(lowcase(candy)) in vars then clean_candy=var;
      end;
   if missing(clean_candy) then clean_candy="UNK";
run;
%mend;
* creates a SAS format that categorizes or bins character data;
* must include at least two comma-delimited items;
* the first of which is the new format and the second (or additional);
* which will be converted to the first value;
%macro ingest_format(fil= /* logical location of format file */,
   form= /* SAS character format being created */);
filename zzzzzzzz "&fil";
data ingest_format (keep=FMTNAME START LABEL);
   length FMTNAME $32 START $50 LABEL $50;
   fmtname="&form";
   f=fopen("zzzzzzzz");
   rc1=fsep(f,",");
   do while(fread(f)=0);
      rc3=fget(f,label);
      label=strip(label);
      do while(fget(f,start)=0);
         start=strip(start);
         output;
         end;
      end;
run;
proc format library=work cntlin=ingest_format;
run;
%mend;
* creates an HTML report that organizes and counts candy by classification;
%macro candy_printer(dsn= /* data set in LIB or LIB.DSN format */);
ods listing close;
ods noproctitle;
ods html path="&outputloc" file="&outputfile" style=sketch;
proc report data=&dsn style(report) = [foreground=black backgroundcolor=black] style(header) = [font_size=2 backgroundcolor=white foreground=black];
   title;
   column clean_candy=candy_class clean_candy n dummy;
   define candy_class / group "Classification" format=$candy_class.;
   define clean_candy / group "Candy";
   define n / "Count";
   define dummy / computed noprint "";
   compute dummy;
      if _c3_>&thresh then call define("_c3_","style",         "style=[backgroundcolor=&highlight]");
      endcomp;
run;
ods html close;
ods listing;
%mend;
%read_config(fil=c:permconfig_candy.txt);
%ingest_format(fil=c:permformat_candy_ordering.txt, form=$candy_class);
%candy_cleaner(dsn=perm.raw_candy);
%candy_printer(dsn=perm.clean_candy);

WHAT'S NEXT?

Software stability represents the gateway toward software automation and scheduling because stable, tested software can be reliably executed while defending against unnecessary maintenance. Another benefit of increased stability is the likelihood that software modules will be reused and repurposed in the future. The next chapter introduces software reuse and repurposing and describes reusability principles and artifacts that can more effectively promote software reuse within a team and organization.

NOTES

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset