BreakOnWord: A Macro for Partitioning Long Text Strings at Natural Breaks Richard Addy, Rho, Chapel Hill, NC Charity Quick, Rho, Chapel Hill, NC

Similar documents
PhUSE US Connect 2018 Paper CT06 A Macro Tool to Find and/or Split Variable Text String Greater Than 200 Characters for Regulatory Submission Datasets

Efficient Processing of Long Lists of Variable Names

Clinical Data Visualization using TIBCO Spotfire and SAS

One Project, Two Teams: The Unblind Leading the Blind

Breaking up (Axes) Isn t Hard to Do: An Updated Macro for Choosing Axis Breaks

The Power of PROC SQL Techniques and SAS Dictionary Tables in Handling Data

Cleaning Duplicate Observations on a Chessboard of Missing Values Mayrita Vitvitska, ClinOps, LLC, San Francisco, CA

The Dataset Diet How to transform short and fat into long and thin

Make Your Life a Little Easier: A Collection of SAS Macro Utilities. Pete Lund, Northwest Crime and Social Research, Olympia, WA

A Quick and Gentle Introduction to PROC SQL

A Simple Framework for Sequentially Processing Hierarchical Data Sets for Large Surveys

A SAS Macro to Create Validation Summary of Dataset Report

SAS 9 Programming Enhancements Marje Fecht, Prowerk Consulting Ltd Mississauga, Ontario, Canada

Useful Tips When Deploying SAS Code in a Production Environment

SAS Programming Techniques for Manipulating Metadata on the Database Level Chris Speck, PAREXEL International, Durham, NC

A Macro to Create Program Inventory for Analysis Data Reviewer s Guide Xianhua (Allen) Zeng, PAREXEL International, Shanghai, China

2 = Disagree 3 = Neutral 4 = Agree 5 = Strongly Agree. Disagree

Text Wrapping with Indentation for RTF Reports

Paper HOW-06. Tricia Aanderud, And Data Inc, Raleigh, NC

Greenspace: A Macro to Improve a SAS Data Set Footprint

PharmaSUG 2013 CC26 Automating the Labeling of X- Axis Sanjiv Ramalingam, Vertex Pharmaceuticals, Inc., Cambridge, MA

Validation Summary using SYSINFO

A Tool to Compare Different Data Transfers Jun Wang, FMD K&L, Inc., Nanjing, China

So Much Data, So Little Time: Splitting Datasets For More Efficient Run Times and Meeting FDA Submission Guidelines

PharmaSUG China. Systematically Reordering Axis Major Tick Values in SAS Graph Brian Shen, PPDI, ShangHai

Automatic Indicators for Dummies: A macro for generating dummy indicators from category type variables

SESUG Paper AD A SAS macro replacement for Dynamic Data Exchange (DDE) for use with SAS grid

%check_codelist: A SAS macro to check SDTM domains against controlled terminology

Arthur L. Carpenter California Occidental Consultants, Oceanside, California

Advanced Visualization using TIBCO Spotfire and SAS

Planting Your Rows: Using SAS Formats to Make the Generation of Zero- Filled Rows in Tables Less Thorny

Journey to the center of the earth Deep understanding of SAS language processing mechanism Di Chen, SAS Beijing R&D, Beijing, China

Tales from the Help Desk 6: Solutions to Common SAS Tasks

Using GSUBMIT command to customize the interface in SAS Xin Wang, Fountain Medical Technology Co., ltd, Nanjing, China

CMISS the SAS Function You May Have Been MISSING Mira Shapiro, Analytic Designers LLC, Bethesda, MD

Unlock SAS Code Automation with the Power of Macros

Macro Quoting: Which Function Should We Use? Pengfei Guo, MSD R&D (China) Co., Ltd., Shanghai, China

Foundations and Fundamentals. SAS System Options: The True Heroes of Macro Debugging Kevin Russell and Russ Tyndall, SAS Institute Inc.

Let the CAT Out of the Bag: String Concatenation in SAS 9

CC13 An Automatic Process to Compare Files. Simon Lin, Merck & Co., Inc., Rahway, NJ Huei-Ling Chen, Merck & Co., Inc., Rahway, NJ

ABSTRACT INTRODUCTION MACRO. Paper RF

Create a Format from a SAS Data Set Ruth Marisol Rivera, i3 Statprobe, Mexico City, Mexico

Program Validation: Logging the Log

STEP 1 - /*******************************/ /* Manipulate the data files */ /*******************************/ <<SAS DATA statements>>

KEYWORDS Metadata, macro language, CALL EXECUTE, %NRSTR, %TSLIT

Dictionary.coumns is your friend while appending or moving data

%Addval: A SAS Macro Which Completes the Cartesian Product of Dataset Observations for All Values of a Selected Set of Variables

Keeping Track of Database Changes During Database Lock

Error Trapping Techniques for SCL. Andrew Rosenbaum, Trilogy Consulting, Kalamazoo, MI

Give me EVERYTHING! A macro to combine the CONTENTS procedure output and formats. Lynn Mullins, PPD, Cincinnati, Ohio

A Macro that can Search and Replace String in your SAS Programs

SD10 A SAS MACRO FOR PERFORMING BACKWARD SELECTION IN PROC SURVEYREG

Implementing external file processing with no record delimiter via a metadata-driven approach

Submitting SAS Code On The Side

Taming a Spreadsheet Importation Monster

Simplifying Your %DO Loop with CALL EXECUTE Arthur Li, City of Hope National Medical Center, Duarte, CA

Making the most of SAS Jobs in LSAF

Data Manipulation with SQL Mara Werner, HHS/OIG, Chicago, IL

Using PROC REPORT to Cross-Tabulate Multiple Response Items Patrick Thornton, SRI International, Menlo Park, CA

Are you Still Afraid of Using Arrays? Let s Explore their Advantages

Cover Your Assumptions with Custom %str(w)arning Messages

SAS Macro. SAS Training Courses. Amadeus Software Ltd

Using Templates Created by the SAS/STAT Procedures

ABSTRACT INTRODUCTION TRICK 1: CHOOSE THE BEST METHOD TO CREATE MACRO VARIABLES

Introduction. Getting Started with the Macro Facility CHAPTER 1

QUEST Procedure Reference

Using a Control Dataset to Manage Production Compiled Macro Library Curtis E. Reid, Bureau of Labor Statistics, Washington, DC

The Path To Treatment Pathways Tracee Vinson-Sorrentino, IMS Health, Plymouth Meeting, PA

What Do You Mean My CSV Doesn t Match My SAS Dataset?

Submission-Ready Define.xml Files Using SAS Clinical Data Integration Melissa R. Martinez, SAS Institute, Cary, NC USA

SAS Application to Automate a Comprehensive Review of DEFINE and All of its Components

A Time Saver for All: A SAS Toolbox Philip Jou, Baylor University, Waco, TX

Data Edit-checks Integration using ODS Tagset Niraj J. Pandya, Element Technologies Inc., NJ Vinodh Paida, Impressive Systems Inc.

SAS Graphs in Small Multiples Andrea Wainwright-Zimmerman, Capital One, Richmond, VA

Quick and Efficient Way to Check the Transferred Data Divyaja Padamati, Eliassen Group Inc., North Carolina.

Cover the Basics, Tool for structuring data checking with SAS Ole Zester, Novo Nordisk, Denmark

9 Ways to Join Two Datasets David Franklin, Independent Consultant, New Hampshire, USA

Cleaning up your SAS log: Note Messages

A SAS Macro to Generate Caterpillar Plots. Guochen Song, i3 Statprobe, Cary, NC

While You Were Sleeping, SAS Was Hard At Work Andrea Wainwright-Zimmerman, Capital One Financial, Inc., Richmond, VA

Let s Get FREQy with our Statistics: Data-Driven Approach to Determining Appropriate Test Statistic

A Practical and Efficient Approach in Generating AE (Adverse Events) Tables within a Clinical Study Environment

Use That SAP to Write Your Code Sandra Minjoe, Genentech, Inc., South San Francisco, CA

Anatomy of a Merge Gone Wrong James Lew, Compu-Stat Consulting, Scarborough, ON, Canada Joshua Horstman, Nested Loop Consulting, Indianapolis, IN, USA

Countdown of the Top 10 Ways to Merge Data David Franklin, Independent Consultant, Litchfield, NH

An Introduction to Stata Exercise 1

Techniques for Writing Robust SAS Macros. Martin Gregory. PhUSE Annual Conference, Oct 2009, Basel

Using SAS 9.4M5 and the Varchar Data Type to Manage Text Strings Exceeding 32kb

A SAS Macro Utility to Modify and Validate RTF Outputs for Regional Analyses Jagan Mohan Achi, PPD, Austin, TX Joshua N. Winters, PPD, Rochester, NY

SAS Macro Technique for Embedding and Using Metadata in Web Pages. DataCeutics, Inc., Pottstown, PA

In this paper, we will build the macro step-by-step, highlighting each function. A basic familiarity with SAS Macro language is assumed.

A Macro to Keep Titles and Footnotes in One Place

SDTM Attribute Checking Tool Ellen Xiao, Merck & Co., Inc., Rahway, NJ

EXAMPLE 3: MATCHING DATA FROM RESPONDENTS AT 2 OR MORE WAVES (LONG FORMAT)

BI-09 Using Enterprise Guide Effectively Tom Miron, Systems Seminar Consultants, Madison, WI

e. That's accomplished with:

WHAT ARE SASHELP VIEWS?

A Macro to replace PROC REPORT!?

ABSTRACT: INTRODUCTION: WEB CRAWLER OVERVIEW: METHOD 1: WEB CRAWLER IN SAS DATA STEP CODE. Paper CC-17

Purchase this book at

Transcription:

PharmaSUG 2014 - Paper CC20 BreakOnWord: A Macro for Partitioning Long Text Strings at Natural Breaks Richard Addy, Rho, Chapel Hill, NC Charity Quick, Rho, Chapel Hill, NC ABSTRACT Breaking long text strings into smaller strings can be tricky, if splitting the string needs to be based on something more than simply length. For example, in SDTM domains, character variables are limited to 200 characters. Excess text need to be placed into supplemental datasets (and the variables there are also limited to 200 characters) - but it s not sufficient to just break the text into 200-character chunks; the text needs to be split between words. This paper presents the BreakOnWord macro, which breaks a long text variable into a set of smaller variables of a length specified by the user. The original text is partitioned text at natural breaks - spaces, as well as user-supplied character values. The macro will create as many variables as necessary, and the variables are named using a usersupplied prefix and an ascending suffix. The user has the option of naming the series of variables beginning with the prefix only (creating SDTM-friendly variable names: AETERM, AETERM1, AETERM2, for example). BreakOnWord checks that the user inputs are valid (the specified data set exists, and the specified long text string is present in that data set) and that the variables to be created by the macro are not already present. The newly created variables are added to the input data set, or, optionally, the macro can output a new data set. BreakOnWord requires SAS of 9.1 or higher. INTRODUCTION When designing and creating SDTM datasets, the maximum length of a character variable is 200 characters. Longer strings need to be broken into smaller strings, and there is a strong preference to use natural breaks when the smaller strings are created i.e., not to just break the original string into 200 character sections. All long text strings in a SDTM conversion should be handled in this fashion, so it seemed appropriate to create a macro to handle the work. While the macro was initially designed to handle long strings based on restrictions in SAS datasets, we decided to generalize it to handle strings with a maximum length provided by the user. This paper introduces a macro that can be called in the body of a SAS program to modify a dataset that contains one or more lengthy variables. The macro should be called once for each variable to be split, and should be called in open code, outside of a DATA step. The macro features parameters that allow the user to specify the name, label, and length of the output variables as well as any special characters (in addition to a space) that should be used to determine break points. As many new variables as are required to break the original string into smaller strings will be created in the output data set. MACRO PARAMTERS DSETIN DSETOUT INSTRING MAXLEN OUTBASE OUTLABEL BLANKSTART Name of input dataset. This is a REQUIRED parameter and there is no default Name of input dataset. This is a REQUIRED parameter and the default is &DSETIN Name of long text string to parse. This is a REQUIRED parameter and there is no default Prefix to be used in newly created variables. This is a REQUIRED parameter and there is no default Prefix label for newly created variables. Parameter call should just use text. This is a REQUIRED parameter and there is no default. Prefix label for newly created variables. Default is OUTBASE. Parameter call should use just text and no quotes [YES/NO] YES: First created variable is named &OUTBASE and first created label is &OUTLABEL NO: First created variable is named &OUTBASE.1and first created label is &OUTLABEL.1 This is a REQUIRED parameter and the default is YES 1

DROPSPACE BRKCHARS [YES/NO] Drop the breaking character if that character is a space (i.e., trim the output vaiable. This is a REQUIRED parameter and the default is YES List of characters to be included as break characters in addition to a space. This is an OPTIONAL parameter and there is no default. Single and double quotes cannot be included as an additional breaking character Table 1. BreakOnWord Parameters METHODOLOGY BreakOnWord initially performs some checks on the user-supplied parameters required parameters must be supplied, the data set specified must exist, the variable specified must exist within that data set, and quotation marks cannot be used as breaking characters. Following that, the macro determines how many variables will need to be created. For each distinct value of the input string that is longer than the user-supplied maximum length (&MaxLen), the macro starts by copying the value into a temporary sting. BreakOnWord looks at character &MaxLen +1 if it s a space, the first &MaxLen characters are taken as a chunk (i.e., the substring ends at the end of a word). If there are no breaking characters within the first &MaxLen characters, it s also taken as a chunk. Otherwise, the macro locates the position of the right most breaking character (a space, as well as any additional character supplied by the user), and takes everything up to that as a chunk. The process is repeated on the remaining characters in the string until fewer then &MaxLen characters remain. A running total of the chunks required is kept; if there are no values of the input string greater than &MaxLen, a single new variable will need to be created. Once the macro knows how many variables will need to be created, it goes about actually creating them. The macro checks to see if the variables it will create already exist in the input dataset if they do, and are numeric, the macro stops. If one or more variables already exist, and are shorter than &MaxLen, a warning is generated. Using the same logic as before, BreakOnWord breaks down each value of the input string into smaller chunks, and each chunk is assigned to a new variable. Variables are named based on the user supplied prefix the user can chose to name the variables according to the SDTM standard (first new variable is unnumbered, additional new variables are numbered sequentially) or by numbering all new variables sequentially. If the user wishes, a label is applied to the new variables. The new variables are trimmed of leading and trailing spaces by the DROPSPACE parameter default unless otherwise specified by the user. If none of the newly created variables already exist in the input data set, they will be set to length &MaxLen. BREAKONWORD %macro BreakOnWord( dsetin=, dsetout=, instring=, maxlen=200, outbase=, outlabel=, blankstart=yes, dropspace=yes, brkchars=); %* local macro vars; %local GotData dsid GotVar rc PosTrunc FoundNum BrkChars2 Varlist Lbllist Varlist2 Lblsttm Continue Lname Mname PrevDef NewVar; %* Required parameters are present; %if %nrbquote(&dsetin) = %then %do; %put REQUIRED PARAMETER dsetin DOES NOT EXIST. BREAKONWORD WILL STOP; %if %nrbquote(&instring) = %then %do; %put REQUIRED PARAMETER instring DOES NOT EXIST. BREAKONWORD WILL STOP; %if %nrbquote(&maxlen) = %then %do; %put REQUIRED PARAMETER maxlen DOES NOT EXIST. BREAKONWORD WILL STOP; %if %nrbquote(&outbase) = %then %do; %put REQUIRED PARAMETER outbase DOES NOT EXIST. BREAKONWORD WILL STOP; %if %nrbquote(&blankstart) = %then %do; %put REQUIRED PARAMETER blankstart DOES NOT EXIST. BREAKONWORD WILL STOP; 2

%if %nrbquote(&dropspace) = %then %do; %put REQUIRED PARAMETER dropspace DOES NOT EXIST. BREAKONWORD WILL STOP; %* Input dataset must exist; %if &dsetin= %then %let GotData = NO ; %else %do ; %if %sysfunc(exist(&dsetin)) %then %let GotData = YES; %else %let GotData = NO ; %end ; %if &GotData=NO %then %do ; %put THE INPUT DATA SET &dsetin DOES NOT EXIST. BREAKONWORD WILL STOP; %* Instring must be a variable in the input dataset; %let dsid = %sysfunc(open(&dsetin)); %if (&dsid) %then %do; %if %sysfunc(varnum(&dsid,&instring)) %then %let GotVar = YES ; %else %let GotVar = NO ; %let rc = %sysfunc(close(&dsid)); %if &GotVar = NO %then %do; %put INPUT VARIABLE &instring DOES NOT EXIST. BREAKONWORD WILL STOP; %* Additional break characters can not contain quotes; %let continue=yes; if index("&brkchars", "'") > 0 then do; call symputx('continue', 'NO'); if index("&brkchars", '"') > 0 then do; call symputx('continue', 'NO'); %if &continue=no %then %do; %put ADDITONAL BREAKING CHARACTERS CANNOT INCLUDE QUOTATION MARKS; %* Handle some defaults; %if &dsetout = %str() %then %do; %let dsetout = &dsetin; %let blankstart = %upcase(&blankstart); %let dropspace = %upcase(&dropspace); %* Create a single-quoted, comma separated list of a space and any additional; %* breaking characters provided; %let brkchars2 = ' '; %if &brkchars ne %then %do; brkchars2 = "' '" %do j = 1 %to %length(&brkchars); ", '%substr(&brkchars,&j,1)'" ; call symputx( 'brkchars2', brkchars2); 3

%* Find records where text variable is longer than the length of the output; %* variables; proc sql; create table maxlen as select distinct &instring, length(&instring) as maxval from &dsetin order by maxval ; quit; %* see if there are any values that will need to be broken apart; %let continue = NO; set maxlen; call symputx( 'maxval', strip(put(maxval, best.))); if maxval <= &maxlen then do; call symputx('continue', 'NO'); call symputx('continue', 'YES'); if _N_ = 1 then newvarmax = 0; retain newvarmax; %* see how many variables will be needed to handle current string; _instring = trim(&instring); newvars = 0; done = 0; length _chopped $&maxlen; do while(not done); newvars = newvars + 1; if length(_instring) <= &maxlen then done = 1; _chopped = substr(_instring,1,&maxlen); %* this segment ends on a word - only look for a space; if substrn(_instring, &maxlen + 1, 1) = ' ' then _notch = &maxlen; %* no place to split, so just take the whole segment; else if indexc(_chopped, &brkchars2) = 0 then _notch = &maxlen; %* find right-most instance of a breaking character; _notch = &maxlen - indexc(reverse(_chopped), &brkchars2) +1; _instring = substrn(_instring, _notch + 1); if newvars > newvarmax then newvarmax = newvars; call symputx( 'newvarn', strip (put( newvarmax, best.))); %* if no outlabel parameter specified, use outbase; %if &outlabel = %then %let outlabel = &outbase; %else %let outlabel = &outlabel; %if &continue = NO %then %do; %put NOTE: Maximum text length (&maxval characters) in &dsetin is less than; %put length of string to be created (&maxlen characters); %if &blankstart = YES %then %do; %let newvar = %upcase(&outbase); %let newlbl = %upcase(&outlabel); %else %do; %let newvar = %upcase(&outbase.1); %let newlbl = %upcase(&outlabel.1); 4

data &dsetout; set &dsetin; label &newvar=%upcase(&newlbl); &newvar = &instring; %else %do; %* Maximum length of string is greater than maximum length of text string to; %* be created - at least 2 variables will be created; %* generate a list of variables needed; %if &blankstart = YES %then %do; %let varlist = &outbase; %let lbllist = &outlabel; %let lblsttm = &outbase=&outlabel; %do j = 1 %to &newvarn-1; %let varlist = &varlist &outbase.&j; %let lbllist = &lbllist &outlabel.&j; %let lblsttm = &lblsttm &outbase.&j=&outlabel.&j; %else %do; %let varlist = ; %let lbllist = ; %let lblsttm = ; %do j = 1 %to &newvarn; %let varlist = &varlist &outbase.&j; %let lbllist = &lbllist &outlabel.&j; %let lblsttm = &lblsttm &outbase.&j=&outlabel.&j; %put BREAKONWORD: &newvarn (&varlist) variables will be needed ; %* see if input dataset is a work dataset or not. ; dsetin = upcase("&dsetin"); if index(dsetin, '.') = 0 then do; call symputx('lname', 'WORK'); call symputx('mname', dsetin); call symputx('lname', substr(dsetin, 1, index(dsetin, '.')-1)); call symputx('mname', substr(dsetin, index(dsetin, '.')+1)); * placeholder; %* while we are here, generate a quoted list of variable names; varlist2 = "'%scan(&varlist,1,%str( ))'" %do j = 2 %to &newvarn; ", '%scan(&varlist,&j, %str( ))'" ; call symputx( 'varlist2', varlist2); %* Check to see if variables have been previously defined. If not, will; %* need to define them. Throw a warning; %* if variable is previously defined and length < &maxlength, and error; %* out if its numeric; 5

%* Assume none of the new variables were previously defined; %let PrevDef = NO; %* Assume none of the new variables were previously defined numeric; %let FoundNum = NO; %* Assume none of the new variables were previously defined as short; %let PosTrunc = NO; set sashelp.vcolumn (where=(upcase(libname)="&lname" and upcase(memname) = "&mname")); name = upcase(name); if name in (%upcase(&varlist2)) then do; call symputx( 'PrevDef', 'YES'); if length < &maxlen then do; call symputx( 'PosTrunc', 'YES'); if type = 'num' then do; call symputx( 'FoundNum', 'YES'); %if &FoundNum = YES %then %do; %* Variable needed by the macro has been defined, and it is a number.; %put ONE OR MORE VARIABLES NEEDED BY THIS MACRO (&VARLIST) IS DEFINED AS; %put NUMERIC IN THE INPUT DATASET. BREAKONWORD WILL STOP EXECUTING; %else %do; %if &PosTrunc = YES %then %do; put One or more variables needed for this macro (&VARLIST) has been; %put previously defined as character, with a length less than the; %put maximum length defined for the macro (&maxlen). The macro will; %put continue, but be aware that values may be truncated.; data &dsetout; set &dsetin; label &lblsttm; %if &prevdef = NO %then %do; length &varlist $&maxlen; array bits (&newvarn) &varlist.; %* use similar logic as above, but actually populate variables; length _chopped $&maxlen; if length(&instring) < &maxlen then do; bits(1) = &instring; _instring = trim(&instring); _newvars = 0; _done = 0; length _chopped $&maxlen; do while(not _done); _newvars = _newvars + 1; if length(_instring) <= &maxlen then do; %* leftover does not exceed max length; bits(_newvars) = _instring; _done = 1; 6

%* Find notch - where to split the segment; _chopped = substr(_instring,1,&maxlen); * this segment ends on a word - only look for a space; if substrn(_instring, &maxlen + 1, 1) = ' ' then _notch = &maxlen; %* no place to split, so just take the whole segment; else if indexc(_chopped, &brkchars2) = 0 then _notch = &maxlen; %* find right-most instance of a breaking character; else _notch = &maxlen - indexc(reverse(_chopped), &brkchars2)+ 1; %* assign current segment; bits(_newvars) = substrn(_instring,1, _notch); %* trim if necessary; %if &dropspace = YES %then %do; bits(_newvars) = strip(bits(_newvars)); %* Set up for next pass through loop; _instring = substrn(_instring, _notch+1); %* length(_instring) > &maxlen; %* else do while not done; %* length(&instring) > &maxlen; drop _newvars _done _instring _chopped _notch; %* Clean up; proc datasets lib=work nolist; delete maxlen maxlen2; quit; %* if exit conditions exist, program will skip to this point; %exit: %mend BreakOnWord; NOTES The method BreakOnWord uses to determine how many variables will need to be created is complete, but inefficient. For specific situations (large data sets, cases where &MaxLen is large, etc.), the macro could be made faster by reducing the number of distinct values it looks at when determining how many variables will be needed. However, we felt that for general use, a robust, albeit slower, method of determining the number of new variables was appropriate. By requiring BreakOnWord to be called outside of a DATA step, the macro is able to perform a few tasks more easily: checking to see if the variables that will be created are already present in the input data set, for example. If the output variable is the same as the input variable (For example, if INSTRING=AETERM, OUTBASE=AETERM, BLANKSTART=NO), the input variable will have its values truncated appropriately, but its length will not be reset. CONCLUSION BreakOnWord is a useful macro for breaking apart long string variables into smaller strings at user-specified break points. The user does not need to know how many variables will be needed the macro will determine that for them. BreakOnWord emphasizes robust functionality and features flexible options for variable naming, break character choices, labels, and string trimming. While the macro was developed in response to specific needs found in SDTM conversions, it can be used for other cases where longer strings need to be broken apart. 7

CONTACT INFORMATION Your comments and questions are valued and encouraged. Contact the authors at: Richard Addy Rho 6330 Quadrangle Drive Chapel Hill, NC 27517 919-595-6579 (w) 919-408-0999 (f) richard_addy@rhoworld.com www.rhoworld.com https://twitter.com/rhoworld Charity Quick Rho 6330 Quadrangle Drive Chapel Hill, NC 27517 919-595-6594 (w) 919-408-0999 (f) richard_addy@rhoworld.com www.rhoworld.com https://twitter.com/rhoworld SAS and all other SAS Institute Inc. product or service names are registered trademarks or trademarks of SAS Institute Inc. in the USA and other countries. indicates USA registration. Other brand and product names are trademarks of their respective companies. 8