If you are familiar with TFL programming, you would know page numbers in X of Y format – Page X of Y. “X” is the current page number while “Y” is the total number of pages. There are several approaches to generate “Page X of Y” in a header or footer section. Microsoft Word processor functions such as “{page {\field{\fldinst{page}}} of {\field{\fldinst{numpages}}}}” or “Page ~{thispage} of ~{lastpage}” in TITLE or FOOTNOTE statement are applied most widely to generate page numbers.

This method is wonderful as it prevents us from using do loop to loop through several PROC REPORT procedures. However, it has drawbacks. Here is the code which can be used to produce rtf file containing Page X of Y page numbers.

Click here to hide/show code


proc template;
define style customrtf;
parent = Styles.RTF;

replace fonts /
‘TitleFont’ = (“Times New Roman”,9pt) /* Titles from TITLE statements */
‘TitleFont2’ = (“Times New Roman”,9pt) /* Procedure titles */
‘StrongFont’ = (“Times New Roman”,9pt,Bold)
‘EmphasisFont’ = (“Times New Roman”,9pt)
‘headingEmphasisFont’ = (“Times New Roman”,9pt,Bold)
‘headingFont’ = (“Times New Roman”,9pt) /* Table column and row headings */
‘docFont’ = (“Times New Roman”,9pt) /* Data in table cells */
‘footFont’ = (“Times New Roman”,9pt) /* Footnotes from FOOTNOTE statements */
‘FixedEmphasisFont’ = (“Times New Roman”,9pt)
‘FixedStrongFont’ = (“Times New Roman”,9pt,Bold)
‘FixedHeadingFont’ = (“Times New Roman”,9pt,Bold)
‘BatchFixedFont’ = (“Times New Roman”,9pt)
‘FixedFont’ = (“Times New Roman”,9pt)
;

replace color_list /
‘link’ = blue /* links */
‘bgH’ = _undef_ /* row and column header background */
‘fg’ = black /* text color */
‘bg’ = _undef_; /* page background color */

replace Body from Document /
topmargin = 1.18 in
bottommargin = 1 in
leftmargin = 1 in
rightmargin = 1.18 in
protectspecialchars = off
;

replace Table from Output /
frame = hsides /* outside borders: void, box, above/below, vsides/hsides, lhs/rhs */
rules = groups /* internal borders: none, all, cols, rows, groups */
cellpadding = 0pt /* the space between table cell contents and the cell border */
cellspacing = 0pt /* the space between table cells, allows background to show */
borderwidth = 0.5pt /* the width of the borders and rules */
outputwidth = 100%
;

end;
run;

data class;
set sashelp.class(in=a) sashelp.class(in=b) sashelp.class(in=c);
if a and sex = “F” then pg = 1;
if a and sex = “M” then pg = 2;
if b and sex = “F” then pg = 3;
if b and sex = “M” then pg = 4;
if c and sex = “F” then pg = 5;
if c and sex = “M” then pg = 6;
run;

proc sort; by pg sex age name; run;

ods escapechar=”~” ;
options orientation=landscape nodate nonumber noquotelenmax;
options formchar=”|—-|+|—+=|-/\<>*”;
ods rtf file=”D:/test.rtf” style=customrtf nogtitle;

title1 justify=l “Protocol: XXX” justify=right “{page {\field{\fldinst{page}}} of {\field{\fldinst{numpages}}}}”;
/*title1 justify=l “Protocol: XXX” justify=right “Page ~{thispage} of ~{lastpage}”;*/

proc report data=class;
column pg name sex age height weight;

define pg / order noprint;
define name / “Name” style(column)=[cellwidth = 0.02in];
define sex / “Sex” style(column)=[cellwidth = 0.02in];
define age / “Age” style(column)=[cellwidth = 0.02in];
define height / “Height” style(column)=[cellwidth = 0.02in];
define weight / “weight” style(column)=[cellwidth = 0.02in];

break after pg/page;
quit;

ods rtf close;

Issue Description

Here are the first page and second page of rtf file that we just created. You can see that in the first page, “Page X of Y” displays as “Page 1 of 1”. This is not right as there are 6 pages in total. The right one should be “Page 1 of 6”. If you look at remaining pages, you will find that page numbers are correct in all other pages including the second page.

If you click on “Page 1 of 1” or scroll through the whole file, “Page 1 of 1” will be replaced with “Page 1 of 6”. Some sponsors may want page numbers to be displayed correctly all the time. In this case, what should we do?

A Novel Way to Compute Page Numbers

Once upon a time, I got the same request and after a lot of attempts, I found below novel approach. This method requires basic knowledge of RTF code. You can read my previous post to get required knowledge or refresh your mind. Generally speaking, we need to import RTF file into SAS dataset and then count how many number of “\sectd” in the whole SAS dataset. Why “\sectd”?  “\sectd” indicates that RTF reader needs to reset page to default properties and it only appears once in each page.

Here is the full code. First of all, we have to import rtf file into SAS dataset. After importing, we can count number of “sectd” and create “Page X of Y” based on variables pg and totpg. Variable pg is current page number while totpg is the total page number. Finally, we need to replace “{page {\field{\fldinst{page}}} of {\field{\fldinst{numpages}}}}” (in our example) or “Page ~{thispage} of ~{lastpage}” with user-defined “Page X of Y” – variable pgc.

Click here to hide/show code


data rtf;
length rtf code $32767;
infield “D:/test.rtf” lrecl=32767 end=eof;
input;
rtfcode=_infile_;
run;

data rtf;
retain pg;
set rtf;
obs = _n_;
if _n_ = 1 then pg = 0;
if index(rtfcode,’\sectd’) gt 0 then pg + 1;
run;

proc sort data = rtf; by descending obs; run;

data rtf;
retain totpg;
set rtf;
by descending obs;
if _n_ = 1 then totpg=pg;
run;

proc sort data = rtf; by obs ; run;

data rtf;
set rtf;

pgc = “Page “||strip(put(pg,best.))||” of “||strip(put(totpg,best.));

if index(rtfcode, “Page 1 of 1”) then rtfcode = tranwrd(rtfcode, “Page 1 of 1”, pgc);
run;

data _null_;
file “D:/test.rtf” lrecl = 32767 ;
set rtf;
put rtfcode;
run;


This figure shows you which part of the SAS dataset will be changed by running above code. Blue square shows you where to find “\sectd”. At this moment, if you re-open test.rtf, you will find that the page numbers in the first page is “Page 1 of 6”.