Is a string a %List?

Determining if a given string qualifies as a Caché %List is non-trivial, as Caché itself provides no (documented) function for this purpose. A undocumented function in ^%occRun is available:

 Write $$islist^%occRun(Variable)

However, this function seems to do minimal checks (it apparently merely checks whether the list item lengths add up). So, for example, in the following code, a string is built which, according to islist^%occRun, is a valid list, but which in fact returns an error when trying to retrieve an item from it (i.e., it is not a %List).

 Set NoList="2"_$J("",49)
 Write $$islist^%occRun(NoList),! ;Returns 1
 Write $ListGet(NoList),! ;Causes an error

Another commonly used way to determine if something is a list is to use errortrapping in combination with the use of the $ListLength and/or $ListFind functions, e.g.:

IsList(String)
  Set $ZT="NoList"
  If $ListLength(String)
  If $ListFind(String,"dummy string")
  Quit 1
NoList
  Quit 0

The $ListLength test is equivalent to islist^%occRun, the additional check using $ListFind adds some safety; however, a string that can be considered a valid list if only the length part of the list items is checked, contains an item with the string searched for (here: "dummy string"), and has a non-list item after the item that contains the value searched for, will return a false positive.

An inmproved version of this code is:

IsList(String)
  New i
  Set $ZT="NoList"
  For i=1:1:$ListLength(String) If $ListGet(String,i)
  Quit 1
NoList
  Quit 0

I can't think of a situation where this will fail; disadvantages are only the (possibly expensive) use of error trapping, and the performance issues that returning each list item may have on larger strings.

Parsing a %List

For most purposes, the code described above should be sufficient. The version below is intended to illustrate how a %List can be parsed manually. It implements a more thourough check than islist^%occRun; code that passes this test is accessible using Caché's list-related functions. It only uses error trapping for numeric types, which could be an advantage if the test is needed often and will likely fail a significant percentage of the calls. For other item types, it only examines the item length and type.

Note that it is still possible that a binary string, by coincidence, qualifies as a list, even if it is not intended as such. Normal text can never qualify, as a list always has a type byte which is binary.

Based on the background information described below, more comprehensive testing is possible; specifically, numeric types could be checked for validity.

IsList(List) Public
{
  Set $ZTrap="Error"
  If List="" Quit 0

  Set IsList=0
  Set Index=1,ListLength=$Length(List)
  For Item=1:1 {
    Set ItemLength=$Ascii($Extract(List,Index))
    Set Type=$Ascii($Extract(List,Index+1))

    If ItemLength=0 { ;Item of more than 253 characters
      ;Stored length is one more than the item's data = three
      ; less than the overall item length
      Set ItemLength=$Ascii($Extract(List,Index+2))*256+
          $Ascii($Extract(List,Index+1))+3
      Set Type=$Ascii($Extract(List,Index+3))
      If ItemLength=0 Quit

    } ElseIf ItemLength=1 {  ;Null item
      If Index=ListLength Set IsList=1 Quit
      Set Index=Index+1
      Continue
    }

    If Index+ItemLength-1>ListLength Quit

    ;Check the list type:
    If "1245678"'[Type Quit

    ;For numeric types, use an error trap to check validity
    If Type>2,$ListGet(List,Item)

    ;If Index plus length of this item adds up to end of string,
    ; this is a list:
    If Index+ItemLength-1=ListLength Set IsList=1 Quit
    Set Index=Index+ItemLength
  }

  Quit IsList

Error
  Quit 0
}

Background

Caché lists do not use delimiters to separate items; instead, they store information about the length and type of a list item along with the actual data. Lists of more than one item are therefore equivalent to the concatenation of single-item lists with the same contents.

Structure of a list

The basic structure of a list is either:

1 byte length of item (including overhead)
1 byte type of item (see below)
variable item data

(for items of 253 characters or less in length), or:

1 byte binary 0
1 byte type of item (see below)
2 bytes length of item (including overhead) - 3
variable item data

List types

List types can be one of those described below. Note that this list is reverse-engineered, more types may be possible; specifically, I wonder if type 3 is perhaps an UTF-16BE encoded string. Also note that the descriptions of the format are based on testing on Intel Pentium (i.e., little-endian) hardware; I don't have access to a big-endian machine with Caché, so I can't test list contents there.

1 A string
2 A Unicode string (UTF-16LE encoded); see the Wikipedia on UTF-16.
4 A positive integer (stored in binary form, little endian, leading zeroes stripped)
5 A negative integer, (stored in binary form, little endian, leading -1 bytes stripped)
6 A positive floating point number
7 A negative floating point number
8 A 64-bit IEEE floating point number, as created by $Double()

The integer and floating point types store the data in binary format, not as a string. As far as I know, this is the only place where Caché visibly distinguishes between different data types (apart from objects in Caché 5+). I have not examined exactly how floating point values are stored. Note that a list item containing another list has no special type (as far as I could determine), i.e. it is stored as a string (type 1).

Null items

A null item is a list item that is present but has no contents. It is defined by a single binary 1, i.e. a list item of length one.

%Status

Finding out if a variable contains a %Status is easy once the problem of determining if something is a %List is solved. A %Status is either a "1" (for success), or a string "0 " (zero,space) followed by a list of lists; each inner list item represents an error, and must contain four items. The first of those is the error code, the second through fourth are optional parameters to the error, and are present as null items if not applicable. In code:

IsStatus(Status)
  New Item,i,Result

  If Status="1" Quit 1
  If $Extract(Status,1,2)'="0 " Quit 0
  Set Status=$Extract(Status,3,$Length(Status))
  If '$$IsList(Status) Quit 0
  Set Result=1
  For i=1:1:$ListLength(Status) {
    Set Item=$List(Status,i)
    If '$$IsList(Item) Set Result=0 Quit
    If $ListLength(Item)'=4 Set Result=0 Quit
  }
  Quit Result

License

The code and information presented on this page is written by Gertjan Klein, and placed in the public domain; it can be used freely by anyone for any purpose.