30-Day Course
Clinical SAS → R
Day 2 of 30·R Basics & SAS Equivalents

Character Functions: From SCAN, SUBSTR & TRANWRD to stringr

Every string operation you rely on in the DATA step has a clean, readable equivalent in base R or the stringr package.

Learning objectives

  • 1Translate the most common SAS character functions to base R and stringr.
  • 2Understand 1-based indexing and how SUBSTR maps to substr() / str_sub().
  • 3Handle case conversion, trimming, concatenation, and find-and-replace.
  • 4Recognize why stringr's consistent str_* naming makes code easier to review.

Key SAS® → R comparison

TaskSAS®R
SubstringSUBSTR(x, 2, 3)substr(x, 2, 4) / str_sub(x, 2, 4)
UppercaseUPCASE(x)toupper(x) / str_to_upper(x)
LowercaseLOWCASE(x)tolower(x) / str_to_lower(x)
Trim blanksSTRIP(x) / TRIM(x)trimws(x) / str_trim(x)
LengthLENGTH(x)nchar(x) / str_length(x)
ConcatenateCATX(' ', a, b)paste(a, b) / str_c(a, b, sep=' ')
Find positionINDEX(x, 'AE')regexpr('AE', x) / str_locate(x,'AE')
Replace textTRANWRD(x,'mg','MG')gsub('mg','MG',x) / str_replace_all()
Split / tokenSCAN(x, 2, ' ')strsplit(x,' ')[[1]][2] / str_split()

Code examples

Substring and case conversion
SAS®
data ae;
  set ae;
  soc3 = upcase(substr(aedecod, 1, 3));
run;
R
library(stringr)
ae <- ae |>
  mutate(soc3 = str_to_upper(str_sub(aedecod, 1, 3)))

Note: str_sub uses start/end positions (both inclusive), while SAS SUBSTR uses start/length. substr(x,2,4) in R returns characters 2 through 4.

Concatenation and find-and-replace
SAS®
data dm;
  set dm;
  fullname = catx(' ', firstnm, lastnm);
  dose_u   = tranwrd(dose, 'mg', 'MG');
run;
R
dm <- dm |>
  mutate(
    fullname = str_c(firstnm, lastnm, sep = " "),
    dose_u   = str_replace_all(dose, "mg", "MG")
  )

Note: str_c with sep mirrors CATX. By default str_c returns NA if any input is NA — use str_replace_na() if you need SAS-style blank handling.

Tokenizing with SCAN vs. str_split
SAS®
data _null_;
  term = "RANDOMIZED-DOUBLE-BLIND";
  word2 = scan(term, 2, '-');
  put word2=;   /* DOUBLE */
run;
R
term  <- "RANDOMIZED-DOUBLE-BLIND"
word2 <- str_split(term, "-")[[1]][2]
word2   # "DOUBLE"

Note: str_split returns a list; index [[1]] to get the vector of tokens, then [2] for the second word.

Explanation

Character handling is where many SAS programmers first feel at home in R, because the operations map almost one-to-one. The two things to internalize are indexing and the stringr naming convention. R, like SAS, is 1-based — the first character is position 1 — but substr() takes a start and an end position rather than a start and a length, so SUBSTR(x,2,3) becomes substr(x,2,4).

The stringr package gives every function a str_ prefix, takes the string as the first argument, and returns predictable types. This consistency matters enormously in a regulated environment where code is read and reviewed far more often than it is written. Base R equivalents (toupper, gsub, nchar) work fine and require no package, but stringr code is usually clearer in a pipeline.

One genuine difference: SAS character variables are fixed-length and blank-padded, so trailing spaces appear constantly and STRIP/TRIM are everyday tools. R strings are variable length with no padding, so you trim far less often — usually only when cleaning imported data.

Practice exercise

Given USUBJID values like 'STUDY01-101-0007', use stringr to extract the site number ('101') and the subject number ('0007'), convert a free-text AETERM column to title case, and replace every occurrence of 'NAUSEA' with 'Nausea'. Compare your str_* solution to the equivalent SCAN/SUBSTR/TRANWRD approach.