Developing Applications in DATATRIEVE
Browsing under Program Control

Joe H. Gallagher, Ph. D.



One of DATATRIEVE's strongest features is the ability to browse through data interactively. With alternate use of FIND with an RSE (record selection expression) and PRINT, one can rather quickly narrow in on one or a few records within a domain. The ability to discriminate records requires that the user observe the records collected in each FIND, and then form a more restrictive record selection expression. The form of this more restrictive record selection expression is not pre-specified, and may be any one or a combination of the allowable Boolean expression in DATATRIEVE. It may take the interactive user one or many attempts to isolate the record or records of interest.

Under program control (as within a DATATRIEVE procedure that can be run within a menu system), the form of record selection expressions must be limited to a few possibilities. Oner could certainly not afford the procedure compile time if one attempted to program all possible record selection expressions or combinations thereof. In addition, most, if not all, procedures will be coded with selection and display of candidate records within a BEGIN-END block so that multiple attempts may be make. Of course, the use of a BEGIN-END block precludes the use of the FIND statement and collections.

So how can one browse through a data base under program control without using a FIND statement and making one or more collections? There are two possible ways to accomplish this: first, candidate records for part of candidate records) could be written to a temporary domain; and second, a key or pointer to candidate records could be created with memory in a temporary global variable. Transferring candidate records to a temporary domain is unlikely to be a big performance win; the overhead of initializing a new file or erasing the previous version and then copying records (or part of records) to a temporary file will take a significant amount of time. Such a slow method which uses disk space will not be considered further. Creating memory resident keys or pointers using global variables is fast, but tricky. Because there are no arrays in DATATRIEVE (Note: There really are arrays in the plot language of DATATRIEVE, but these arrays are can not be effectively using at the DATATRIEVE command level.), the DATATRIEVE programmer will have to create and manage such pointers in a large global variable.

To illustrate how such a global variable might be used as a pointer, consider the following domain and record definitions which are part of a (fictitious) library data base system:


define domain SUBJECTS using SUBJECTS-RECORD on SUBJECTS.dat;
define record SUBJECTS-RECORD using optimize
01 SUBJECTS-rec.
   03 SUBJECT PIC X(10).            ! primary key
   03 SUBJECT-DESCRIPTION PIC X(40).
;

Now, a simplified procedure to browse this data would look like:


define procedure BROWSE
ready SUBJECTS shared read
declare buff pic x(200).
declare counter usage is integer.
declare cnt usage is integer.
declare selection usage is integer
    valid if selection between 0 and counter.
declare answer pic x.
declare tsubject pic x(10).
declare subject-key pic x(10).
declare tdescription pic x(40).
answer = "N"
while (answer ne "Y") begin
  print "<ESC>[H<ESC>[J"      ! clear the screen
  print "Browsing the SUBJECT Data Base", skip,
        "Enter only one search criteria."
  tsubject = *."first part of subject code or a blank"
  tdescription = *."phrase within the description or blank"
  buff = ""
  counter = 0
  if (tsubject ne " " and tdescription eq " ") then begin
    for SUBJECTS with SUBJECT starting with tsubject||"" begin
       counter = counter + 1
       buff = fn$str_extract(buff,1,10*counter)|SUBJECT   ! <--
       end
     end
   if (tsubject eq " " and tdescription ne " ") then begin
     for SUBJECTS with SUBJECT-DESCRIPTION count tdescription||"" begin
       counter = counter + 1
       buff = fn$str_extract(buff,1,10*counter)|SUBJECT   ! <--
       end
     end
!
! For simplicity, I have only considered the case when we "find" several
! records.  In actual practice, one must consider the cases when one "finds"
! none, 1, between 1 and 20 (the number which can easily be displayed on a
! CRT screen), and more than 20 (too many to display easily on a screen).
!
  cnt = 0
  repeat counter begin
    cnt = cnt + 1
    for SUBJECTS with subject = fn$str_extract(buff, 10*cnt - 9, 10) begin
      print cnt("Selection") using zz9, SUBJECT, SUBJECT-DESCRIPTION
      end
    end
  print skip
  selection = *.selection or 0 to try again"
  if (selection eq 0) then begin
    answer = "N"
    else else begin
    print SUBJECT, SUBJECT-DESCRIPTION of SUBJECTS with
        SUBJECT = fn$str_extract(buff, 10*selection - 9, 10)
    answer = fn$upcase(*.'"Y" if this is the correct choice')
    end
  end
!
subject-key = fn$str_extract(buff,10*selection - 9, 10)
!
! subject-key now contains the primary key of the record which 
! has been selected by the "browsing" process and it can easily
! be modified, deleted, or otherwise manipulated
. . .
. . .
end-procedure

Well, the procedure BROWSE looks like it will do just what we want. We can search on a "STARTING WITH" on the subject code or a "CONTAINING" in the description. And we can do it over and over again.

The problem is that NONE OF THIS WORKS! The line of code (which was marked with "! <--")


      buff = fn$str_extract(buff, 1, 10*counter) | subject

doesn't work the way we think it does. The default length of a string is 30 characters; if counter is greater than 3, buff is not computed correctly. So it is not possible to construct keys for more than three candidate records. That is, the string extract function has an implied edit string of the form


      format fn$str_extract(buff,1,10*counter) using x(n)

where n is 30 or less. One can not overcome this limitation since a format statement like


      format fn$str_extract(buff,1,10*counter) using x(10*counter)

is illegal!

If the field SUBJECTS is always exactly 10 characters (never any trailing blanks), then we can change the code to


      buff = buff || SUBJECT

However, this works only if the key field never has any trailing blanks.

The only way around this impasse is to create a new DATATRIEVE function which will solve this problem. There is a string function in the VMS library which will do exactly what is needed. This function is STR$REPLACE. Its arguments are the output string, the input string, the starting position in the input string, the ending position in the input string, and the replacement string. Check your VMS documentation, in the VAX/VMS Run-Time Library Routines Reference Manual (continued). Volume 8C, page RTL-822. The code which must be added to the DATATRIEVE function definition macro library file DTR$LIBRARY:DTRFND.MAR is


!
! FN$STR_REPLACE- String replace
!
$DTR$FUN_DEF FN$STR_REPLACE, STR$REPLACE, 5
    $DTR$FUN_OUT_ARG  TYPE = FUN$K_STATUS
    $DTR$FUN_IN_ARG  TYPE = FUN$K_TEXT, OUT_PUT = TRUE
    $DTR$FUN_IN_ARG  TYPE = FUN$K_DESC, DTYPE = DSC$K_DTYPE_T, ORDER = 1
    $DTR$FUN_IN_ARG  TYPE = FUN$K_REF,  DTYPE = DSC$K_DTYPE_L, ORDER = 2
    $DTR$FUN_IN_ARG  TYPE = FUN$K_REF,  DTYPE = DSC$K_DTYPE_L, ORDER = 3
    $DTR$FUN_IN_ARG  TYPE = FUN$K_DESC, DTYPE = DSC$K_DTYPE_T, ORDER = 4
$DTR$FUN_END_DEF

This new function is added to DATATRIEVE by setting default to DTR$LIBRARY, editing changes to DTRFND.MAR, assembling DTRFND.MAR, adding the new verion of DTRFND.OBJ to the DTRFUN library, and re-linking the shareable DATATRIEVE image with the command file DTRBLD.COM as show below.


$ set default dtr$library
$!
$!      edit changes to dtrfnd.mar show above
$!
$ macro dtrfnd
$ library/replace dtrfnd dtrfnd
$ @dtrbld.com

The procedure BROWSE is now changed in two places where


        buff = fn$str_extract(buff,1,10*counter)|SUBJECT

is replaced by


        buff = fn$str_replace(buff,10*counter - 9,10*counter,SUBJECT)

In a real application (rather than a simplified example shown here), the size of the in-memory global variable, BUFF, can be extended to a much large size if needed. Also, it is highly advisable to make sure that one does not try to store keys in the buffer past the end of the variable which was specified in the DECLARE.

The procedure BROWSE illustrates the principle of browsing, but the full impact of such a technique can be realized only in multi-level browsing. Suppose the books in our (fictitious) library are "keyed" by SUBJECT, AUTHOR, and TITLE. A partial record definition might look like,


01 BOOKS-REC.
   03 SUBJECT      PIC X(10).
   03 AUTHOR       PIC X(10).
   03 BOOK-CODE    PIC X(10).
   03 TITLE        PIC X(60).
   . . .
;

01 AUTHORS-REC.
   03 AUTHOR-CODE  PIC X(10).
   03 AUTHORS-NAME PIC X(35).
   . . .
;


and would include the SUBJECTS record from the example above. There might typically be 100,000 record in the BOOKS domain; 5,000 records in the AUTHORS domain, and 250 records in the SUBJECTS domain. Clearly, one does not want to browse initially the BOOKS domain; there are just too many record and a careless mistake which is equivalent to "FIND BOOKS WITH TITLE CONT 'E'" would seriously discourage browsing. A better strategy would be to browse in either the SUBJECTS domain or the AUTHORS domains until one had a SUBJECT code or AUTHOR code to restrict the browsing in the BOOKS domain.

Procedures to browse through multi-level, complex data bases are certainly not trivial and may be several hundred lines of DATATRIEVE statements. However, I hope that you can see the great utility that can be accomplished by browsing under program control.


Originally published in the newsletter of the DATATRIEVE/4GL SIG, The Wombat Examiner and 4GL Dispatch, Volume 9, Number 6, pages 10-14; in the Combined SIGs Newsletters of Digital Equipment Computer Users Society, Volume 3, Number 6, February 1988.
Joe H. Gallagher, Ph. D.
dtrwiz@ix.netcom.com
MAILTO

BACK Back