OmniMark Programming Principles

www.serverside.com.au

Chapter 5
Other OmniMark Programming Features.


[Back to the General Index] [Back to the Chapter Summary]

This chapter covers:

Topic Index

5.1: OmniMark Shelfs

An array in any other language is called a shelf in OmniMark and, although the two are similar in many ways there is one important difference. In most languages a programmer must know the size required for array before it is created. Once created an array remains the same size throughout the running of the program. If the number of cells in the array is too large for the actual number of values, then some cells are unused. Once all the cells are used there is no easy way to add more.

Although an OmniMark shelf can be declared with a size, it is common to simply declare shelfs of 'variable' size, with an optional initial size. As more values need storing, cells are easily added, and cells can be removed if no longer needed. It is possible to store values in existing cells, into newly created cells, or before or after existing cells. These features reveal that the underlying data structure used internally by OmniMark may well be a linked list. However visualising the structure as an array or 'shelf of values' remains most productive for understanding the structure.

5.1.1 General Shelf features

A shelf can be declared, either globally or locally, to contain a certain type of values (stream, counter or switch) and with a variable size which is initially zero. The word 'shelf' is not used. An example could be:

global stream people variable initial-size 0

which declares a shelf called 'people'. This shelf contains no cells. To add a cell we can write

new people

The shelf now contains one cell and to store some data into it we can write

set people to "FT Chan, SPACE Learning Centre, Hong Kong"

It is usually more convenient to create a new shelf cell and assign a value to it in one step. If we write

set new people to "Joseph Fong, City University, Hong Kong"

Then the shelf has a new cell assigned the value above. The shelf now contains two cells and if we write

output people

or

output "%g(people)"  ; people is a stream shelf

we get the last cell's value. It is a principle of OmniMark shelfs that the value obtained by using a just shelf name is the value of its last cell.

5.1.2 Indexed Shelfs

Even though no cell numbers were used in the above fragments, the shelf 'people' is still numerically indexed, like arrays in other languages. The first cell has an index of 1 (not zero, as in C, C++ or Java). In OmniMark access to a particular cell can be obtained using the word 'item' followed by the index of the cell required, as shown below:

  output people item 1  ; FT Chan, SPACE Learning Centre, Hong Kong
  output people item 2  ; Joseph Fong, City University, Hong Kong

5.1.3 Keyed Shelfs

It is often more effective to refer to the cells in a shelf with words instead of numerical indices. OmniMark shelfs can be 'keyed' with unique words which allows them to be used as 'associative arrays' (each cell value associated with a key word). Assuming the people shelf had no cells, we could create two cells, each with a value and a key, as follows:

set new people key "chan" to "FT Chan, SPACE Learning Centre, Hong Kong"
set new people key "fong" to "Joseph Fong, City University, Hong Kong"

in which case we could output any cell value by referring to its key:

output people key "fong"

or

output people key "chan"

To output the key value instead of the cell value, we can write

output key of people

which would produce "fong" since the shelf's default position is its last cell. We could output the key of any cell by referring to its item index.

output key of people item 1      ; 'chan'

It is possible to test a shelf to see if it has a certain key:

do when people has key "chan"
  ...
else
  ...
done

or to find out the number of cells in a shelf

output number of people

5.1.4 Traversing a shelf

A shelf traversal can be done with a special 'repeat over' loop. This structure visits each cell of a shelf without specific reference to its item number or key. A counter variable to control iterations of the loop is not required. For example, to output all the values of the people shelf, each on a separate line, we could simply use

repeat over people
  output people join "%n"
again

To output both keys and values, we could use

repeat over people
  output key of people
  output ":"
  output people join "%n"
again

which produces.

chan:FT Chan, SPACE Learning Centre, Hong Kong
fong:Joseph Fong, City University, Hong Kong

The example program shows how a keyed counter shelf can be used. The program submits a text file to a pattern matching rule which finds any vowels in the file and uses a shelf to store the frequency of each vowel. When a vowel is found, a test is performed on the shelf to see if this vowel has been located before. This is done by testing whether the shelf already has that vowel as a key. If it has, the cell in the corresponding cell is increased. When a previously undetected vowel is found, a new cell is created using the vowel as a key and initialised to 1.

[Code Sample: C05T01a.xom]

001  ; Count vowels
002  
003  global counter vowels variable initial-size 0
004  
005  process
006    submit file "../source/chap04.sgml"
007    repeat over vowels
008      output key of vowels join ":" join "%d(vowels)%n"
009    again
010  
011  find ul["aeiou"] => char
012    local stream upChar
013    set upChar to "%ux(char)"
014    do when vowels has key upChar
015      increment vowels key upChar
016    else
017      set new vowels key upChar to 1
018    done
019  
020  find any

When I run the above program, using the source file for chapter 4 of this book, I get the following output:

O:1824
E:3738
A:2251
I:1532
U:921

In the above program, the rule on line 11 will fire each time a vowel character (upper or lower case a,e,i,o or u) is found in the stream. This vowel is captured in the pattern variable 'char'. On lines 12 and 13 a local stream is used to hold the uppercase version of the vowel. The selection from lines 14 to 18 checks if the shelf 'vowels' already has a cell with this key and, if it does increments the cell value. If there is no cell with the current key, a new cell is created on line 17 with the initial value 1.

When the input stream is exhausted, control returns to line 7 where a 'repeat over' loop is used to traverse the shelf and output the keys and values of all its cells.

Topic List

5.2: Buffers and files

Streams, first discussed in Chapter 2, can be used in several ways, including simple strings, accumulative in-memory buffers and as pointers to disk files.

5.2.1 Setting a stream's value

A stream can be assigned a simple string of characters like this:

process
  local stream simpleStream
  set simpleStream to "I don't want a pickle%n"

Such a stream can be have its value changed by similar assignments or can have more data appended like this:

process
  local stream simpleStream
  set simpleStream to "I don't want a pickle%n"
  set simpleStream
    to simpleStream join "I just want to ride my motorcycle."

5.2.2 Writing to a buffer

If your main use for a stream is to always add more data to it, the above 'join' method can be tedious. A better solution in this case is to open the stream as an in-memory 'buffer' and then sequentially 'put' data into it. Each time new data is 'put', it is appended to the existing data. The following program shows how a stream buffer can be used in conjunction with a scanning routine to remove all the commas from a string so the result can be used later in calculations.

[Code Sample: C05T02a.xom]

001  ; Remove commas from a number
002  ; Uses a stream buffer
003  
004  process
005    local stream inputData initial {"2,456,543,444,000"}
006    local stream noCommas
007  
008    open noCommas as buffer
009    repeat scan inputData
010      match ","
011        ; consume each comma
012      match any => aDigit
013        put noCommas aDigit  ;; append next digit
014    again
015    close noCommas
016  
017    output "Input stream is %g(inputData)%n"
018    output "Buffer contains %g(noCommas)%n"

The results can be checked by looking at the output, which is

Input stream is 2,456,543,444,000
Buffer contains 2456543444000

The three steps required to use a stream buffer are

5.2.3 Writing to a file

A disk file can be used to store stream data just as easily as an in-memory buffer and the syntax is almost identical. The steps involved are:

The following fragment of code declares a local stream called 'dataFile' and attaches it to a disk file called 'results.dat':

001  process
002    local stream dataFile
003    open dataFile as file "results.dat"

Once this is done we can write onto the file by using a 'put' action on the stream. The syntax is shown here as additional steps to the fragment above:

001  process
002    local stream dataFile
003    open dataFile as file "results.dat"
004    put dataFile "Text appearing on the first line of the file%n"
005    put dataFile "This appears on the second line%n"
006    put dataFile "Data for the third line%n"

When all required data is written, the stream (and thus the disk file) should be closed. This last fragment completes the steps:

001  process
002    local stream dataFile
003    open dataFile as file "results.dat"
004    put dataFile "Text appearing on the first line of the file%n"
005    put dataFile "This appears on the second line%n"
006    put dataFile "Data for the third line%n"
007    close dataFile

If the stream variable which points to a file or buffer is declared locally (as above) then it can only be used inside the current rule. To allow all rules to write to the same buffer or file, declare the stream globally, then open it early in the program - perhaps in a 'process-start' rule and close it late in the program - perhaps in a 'process-end' rule.

5.2.4 Redirecting output to a file

In most of the programs seen so far, the generic action 'output' has been used to write out data. The default destination for these output actions is the standard output device which is either the console screen (when using the command line version of OmniMark) or the log window (when using the OmniMark IDE). This is convenient while testing programs or when the amount output is small. Quite often we need to write the entire output of a program onto an external text file and this could be done using the technique demonstrated above, that is, by opening a stream as a file and using a 'put' action where we would previously use the generic 'output' action.

An alternative way to write output to a file is to redirect it. Users of the command line version of OmniMark can do this with the command line option '-of' discussed in Chapter 2, Topic 5 . Users of the IDE don't have access to this option. A technique for redirecting the 'output' action to a disk file is available from within a program, as shown in the following examples.

This program writes its output using the default destination:

[Code Sample: C05T02b.xom]

001  ; Write to standard output
002  
003  process
004    output "Text appearing on the first line%n"
005    output "This appears on the second line%n"
006    output "Data for the third line%n"

When called from the command line with the command

omnimark -sb C05T02b.xom -of results.dat

The output is automatically redirected onto the text file 'results.dat'. To achieve the same results from within the program we can use the following technique:

[Code Sample: C05T02c.xom]

001  ; Redirect output to a file
002  
003  process
004    local stream fileStream
005    open fileStream as file "results.dat"
006    using output as fileStream
007    do
008      output "Text appearing on the first line%n"
009      output "This appears on the second line%n"
010      output "Data for the third line%n"
011    done
012    close fileStream

Lines 4, 5 and 12 show the standard technique for declaring a stream, opening it and attaching it to a file, and closing it. The redirection of standard output to the file is done on line 6 with the 'using output as ...' action. This instruction controls all the actions in the 'do...done' block from line 7 to 11. It is important to realise that any redirection stays in effect for all actions controlled by the 'using output as ...' action. So, for example, an outline of a program which processes an SGML instance and redirects all the output from all the element rules is:

001  ; Redirect output from element rules
002  
003  process
004    local stream dataFile
005    open dataFile as file "results.dat"
006    do sgml-parse document
007     scan file "someinstance.sgml"
008     using output as dataFile
009      output "%c"
010    done
011    close dataFile
012  
013  ; element rules from here down...
014      ; use generic 'output' actions

The above is not a complete program, it just shows the general technique. A fully worked example is provided in the next topic where an SGML instance is processed to output a web page file in HTML.

To complete the discussion, it is easy to append output to an existing file using the action 'reopen' instead of 'open' and we can check if a file exists before using it with the selection

do when file "somefile.dat" exists
  ...

5.2.5 Writing to built-in streams

There are times when we may have redirected the output action to a file but still want some data written to standard output. This can be done by explicitly putting data to the built-in stream '#main-output' as shown in the following program. This program redirects all data written with the 'output' actions to a text file called 'mydata.dat'. However, during this activity a message is written directly to standard output. Note carefully in this situation that if there is no '-of' option supplied on the command line, then the message written to #main-output will appear on our console screen. If a '-of' option has been used then the message will be directed into the file named after that option.

[Code Sample: C05T02d.xom]

001  ; Writing to main output
002  
003  global stream dataFile
004  
005  process
006    open dataFile as file "mydata.dat"
007    using output as dataFile
008    do
009      output "Writing line 1 to the file%n"
010      output "Here is line 2 on the file%n"
011      put #main-output "A message for standard output."
012      output "This is the last line%n"
013    done
014    close dataFile

It is a principle of OmniMark programming that the stream '#main-output' always refers to your standard output device, even if the generic output action has been redirected. Standard output itself can be redirected with the '-of' option on the command line. If you want to ensure that data actually gets written to your console screen, irrespective of any redirection, put the data to the built-in stream '#console', for example

put #console "A message for the screen%n"

Similarly, access to your operating system's standard error stream is also available in OmniMark as a built-in stream. Standard error can be written to with the action

put #error "A message for standard error%n"

Topic List

5.3: Referents

5.3.1 General principles

OmniMark referents make it possible to write data to an output stream before that data is known. This seems impossible, (it is impossible), but OmniMark takes care of the mechanics so that we can apparently do it. OmniMark allows us to output a referent (a place holder), onto the output stream. Then, when the program knows the value the place holder should take, it can be set to the known value.

Using a referent we could output a list of things from an input stream and have a count of how many of these occurred appearing before the list. To demonstrate, suppose we have the text below stored in a file called 'trivial.text'

one
two
three
four
five

We can easily write a program to output all of the lines in this file and then output a message indicating how many lines were processed:

[Code Sample: C05T03a.xom]

001  ; Trvial counting program
002  
003  global counter numLines initial {0}
004  
005  process
006    submit file "trivial.text"
007    output "--------------------------%n"
008    output "%d(numLines) lines were processed%n"
009  
010  find line-start any-text+ => line "%n"
011    increment numLines
012    output "%d(numLines)  %x(line)%n"
013  
014  find any

This program's output is:

1  one
2  two
3  three
4  four
5  five
--------------------------
5 lines were processed

To have the message appear before the lines themselves, we write a similar program which outputs a referent before processing the lines, then sets the referent after processing the lines. Such a program is:

[Code Sample: C05T03b.xom]

001  ; Trvial referent program
002  
003  global counter numLines initial {0}
004  
005  process
006    output referent "XXX"
007    output "--------------------------%n"
008    submit file "trivial.text"
009    set referent "XXX" to "%d(numLines) lines were processed%n"
010  
011  find line-start any-text+ => line "%n"
012    increment numLines
013    output "%d(numLines)  %x(line)%n"
014  
015  find any

for which the output is:

5 lines were processed
--------------------------
1  one
2  two
3  three
4  four
5  five

The technique used here shows the principle of referents. On line 6, a referent is output. This is where a place-holder is written to the output stream. Then, on line 8, the input stream is processed by the find rule which captures, outputs and counts the lines as they are processed. When all the lines are output and counted the place holder (the referent) is given a value (line 9).

5.3.2 Table of Contents

A more useful example of referent use is when a table of contents is required at the top of some output document. The example which follows takes a simple SGML instance and converts it to HTML ready for viewing with a web browser. In the first version of the program, no referents are used and no table of contents is produced. In the second version a referent is used to place a table of contents at the top of the HTML file.

This example uses as input an SGML instance which contains some topics to be discussed at a meeting. Each topic has a name and one or more paragraphs of text following it. The structure conforms to the following DTD which could be stored in a file called 'meeting.dtd':

<!ELEMENT meeting - o (topic+)>
<!ATTLIST meeting date NUMBERS #REQUIRED>

<!ELEMENT topic - o (para+)>
<!ATTLIST topic name CDATA #REQUIRED>

<!ELEMENT para - o (#PCDATA)>

A sample instance is shown here. It contains just a few topics for a meeting on June 28, 2000. This could be stored in a file called 'meeting.sgml'.

<!DOCTYPE MEETING SYSTEM "meeting.dtd">
<MEETING date="28 6 2000">
<TOPIC name="Seminar Organisation">
<para>The next seminar will be held on the first
Wednesday in July. David will contact Mr FT Chan who
is visiting during that week to see if he can present
a seminar.

<TOPIC name="Assessment Committee">
<para>Al wants all staff to use the official template
when submitting distributions for the last assessment
period.
<para>A copy of the spreadsheet can be obtained from
the admin section of the school's web site.

<TOPIC name="Orientation meeting">
<para>A meeting with new clients will be held in the
conference centre towards the end of August. Staff who
will have direct access to new clients are encouraged to
attend the orientation so they can present a brief outline
of the topics covered in their reports.

The following program parses the above instance and directs the output from all the element rules onto a file called 'meeting.html'

[Code Sample: C05T03c.xom]

001  ; Convert SGML to HTML
002  
003  
004  process
005    local stream htmlfile
006    open htmlfile as file "meeting.html"
007    do sgml-parse document
008      scan file "minutes.sgml"
009      using output as htmlfile
010        output "%c"
011    done
012    close htmlfile
013  
014  element meeting
015    output "<HTML>%n<HEAD>%n<TITLE>Meeting Page</TITLE>%n"
016    output "</HEAD>%n<BODY>%n"
017    output "<H2>Meeting of %v(date)</H2>%n"
018    output "%c"
019    output "</BODY>%n</HTML>%n%n"
020  
021  element topic
022    output "<H3>%v(name)</H3>%n"
023    output "%c"
024  
025  element para
026    output "<P>%c</P>%n"

When the file 'meeting.html' is viewed with a web browser, it looks like this:

A modified version of the last program is shown below. Note how a referent is output before all the topics, then the topics are processed and their names collected into a stream and finally the referent is set to the value of the stream when all the topic names are known. A line-by-line description of the referent details is given after the code.

[Code Sample: C05T03d.xom]

001  ; Convert SGML to HTML
002  ; Uses a referent to put a list of topics
003  ; before topics are processed.
004  
005  global stream topicList initial {""}
006  
007  process
008    local stream htmlfile
009    open outfile with referents-allowed as file "minutes.html"
010    do sgml-parse document
011      scan file "minutes.sgml"
012      using output as htmlfile
013        output "%c"
014    done
015    close htmlfile
016  
017  element meeting
018    output "<HTML>%n<HEAD>%n<TITLE>Meeting Page</TITLE>%n"
019    output "</HEAD>%n<BODY>%n"
020    output "<H2>Meeting of %v(date)</H2>%n"
021    output "<HR>%n"
022    output referent "TOC"
023    output "<HR>%n"
024    open topicList as buffer
025    put topicList "<OL>%n"
026    output "%c"
027    put topicList "</OL>%n"
028    close topicList
029    set referent "TOC" to topicList
030    output "</BODY>%n</HTML>%n%n"
031  
032  element topic
033    output "<H3>%v(name)</H3>%n"
034    put topicList "<LI>%v(name)%n"
035    output "%c"
036  
037  element para
038    output "<P>%c</P>%n"

In line 9 of this program the stream 'htmlfile' is attached to a disk file. Note that in this case the stream is explicitly opened using the modifier 'with referents-allowed'. This ensures that OmniMark makes arrangements to correctly store and replace the place-holder in the output stream before the final disk version of the file is closed.

On lines 21, 22 and 23 a referent called 'TOC' is output into the stream positioned after the main heading (line 20) and between to horizontal rule tags in the HTML. This is the position in the output stream which will ultimately hold the table of contents.

On lines 24 to 28, the global stream 'topicList' is opened as a buffer and the open tag for an ordered list is first placed into the buffer. Then the content of the meeting element (all the topics) is processed. When all topics have been processed the end tag for the ordered list is written into the buffer and the buffer is closed. Since the stream 'topicList' contains the table of contents it can be used to resolve the value of the referent - line 29.

Line 26 which processes all the topics causes the element rule for TOPIC to be fired each time a topic is processed. On line 34 the name of each topic is written into the 'topicList' buffer as an HTML list item.

When the output file 'meeting.html' is viewed with a browser the output is:

It is not a very difficult task to modify the program so that in the table of contents each topic is actually a hypertext link to the actual topic as it appears later in the document.

Topic List

5.4: Specifying options in an 'args' file

The Integrated Development Environment is a convenient tool for debugging OmniMark programs but production-level programs are usually best processed with the 'omnimark' executable on the command line. There are many different command line options available and a list of these can be seen by executing the command

omnimark -help

When many options are needed on the command line, typing them all sometimes becomes difficult and error prone. OmniMark provides a single command-line option ('-f') which allows all other options to be read from a file. Suppose we wish to execute OmniMark with these options:

We could do this by entering the command

omnimark -s myprogram.xom -of myoutput.dat -log error.log -d keyword schema

but typing this more than once would be extremely tedious. The alternative is to create a simple text file called 'omargs' (for example), which contains:

-s myprogram.xom
-of myoutput.dat
-log error.log
-d keyword schema

and then calling OmniMark like this:

omnimark -f omargs

Topic List

5.5: OmniMark functions

Functions are available in most languages and can be written is OmniMark using a 'define...function....as' structure. A typical function accepts some data via arguments (parameters) and returns data to its caller. The example below defines a function which accepts two counters and returns the smaller of them:

[Code Sample: C05T05a.xom]

001  ; A simple function
002  
003  define
004   counter function Smaller( value counter num1,
005                             value counter num2)
006   as
007      do when num1 < num2
008        return num1
009      else
010        return num2
011      done
012  
013  process
014    local counter c1 initial {23}
015    local counter c2 initial {12}
016    local counter result
017  
018    set result to Smaller( c1, c2 )
019    output "%d(result) is smaller%n"

The program below contains a function which returns the current date:

[Code Sample: C05T05b.xom]

001  ; A date function
002  
003  define
004   stream function now
005   as
006    local stream ds
007    set ds to date "=n =D, =xY"
008    return ds
009  
010  process
011    output "You ran this program on " join now join "%n"

The last example contains a function which accepts a stream shelf, a stream value and a key value and inserts the value into the shelf with the given key so that all values remain in alphabetical order. The program scans a file containing people's names. As each name is read it is inserted into the shelf. At the end of processing the ordered list of names is displayed.

[Code Sample: C05T05c.xom]

001  ; Keeping a sorted shelf
002  
003  ; ----- Function starts here
004  define function insertToStream
005     ( MODIFIABLE stream data,
006       value stream new-key,
007       value stream new-data )
008  as
009    local stream pos-key
010  
011    ;; check for empty shelf
012    do when number of data = 0
013      set new data key new-key to new-data
014      return
015    done
016  
017  
018    ;; check for append new data
019    do when new-data > data lastmost
020      set new data key new-key to new-data
021      return
022    done
023  
024  
025    ;; general case, search for location to insert
026    repeat over data
027     set pos-key to key of data
028     exit when new-data < data
029    again
030    set new data key new-key before key pos-key to new-data
031  
032  ; ----- Function ends here
033  
034  global stream nameList variable initial-size 0
035  
036  process
037    submit file "names.dat"
038    repeat over nameList
039      output "%g(nameList)%n"
040    again
041  
042  find line-start any-text+ => aName
043    insertToStream( nameList, aName, aName )
044  
045  find any

If the input file 'names.dat' contains

Claudius
Agripena
Penelope
Athene
Clio
Pandora
Archimedes

Then the output will be

Agripena
Archimedes
Athene
Claudius
Clio
Pandora
Penelope

Since the 'insertToStream' function above requires both a value to insert into the ordered shelf and a key for that cell in the shelf, an error will be caused if the same key is used twice. Since my example uses each name as both a key and a value, the program would generate an error if two names were identical. To fix this problem, the program could be modified so that unique keys were generated within the program. Another approach is to modify the function so that keys are not associated with values at all.

Topic List

5.6: Using the OmniMark libraries

5.6.1 Include files

A collection of functions can be stored in a file separate to the program which uses them. Files which contain collections of functions are usually called include files and their filenames have the extension 'xin'. If you have several functions stored in a separate file called 'myfunctions.xin' then these can be used by placing

include "myfunctions.xin"

early in your programs. The above assumes that the file 'myfunctions.xin' is in the current working directory. If your include files are elsewhere on your system, you need to specify their complete pathname.

It is quite convenient to store all your 'xin' files together in one place on your file system. You can use the '-i' command line option to tell OmniMark where these files are. For example, the command

omnimark -sb myprog.xom -i "C:\Data\includes\"

tells OmniMark to look in the directory 'C:\Data\includes' for any include files which are specified in the program.

5.6.2 OmniMark libraries

When you install OmniMark on your system, several useful libraries of functions are installed with it.

Although the early versions of OmniMark provided only pattern matching and markup language facilities, the libraries now make it a very general purpose language. OmniMark now includes libraries for working with dates, file system utilities, database connectivity, CGI programming and so on. This topic covers the general principles involved with using these libraries and includes some sample programs which demonstrate just a few of them.

The OmniMark includable functions are installed in a subdirectory called 'xin' under the main directory where the language is installed. Also, many of these includable functions depend on platform specific dynamic linked libraries of external functions which are installed in a subdirectory called 'lib' under the main installation directory. When you want to use the libraries, the first thing to do is to identify the pathname to the 'xin' and 'lib' subdirectories. On my system, the full path to the 'xin' directory is

"C:\Program Files\OmniMark\xin\"

and the full path to the dynamic linked libraries is

"C:\Program Files\OmniMark\lib\"

Note that I have placed the above paths within quotation marks. This is necessary on my system since the directory 'Program Files' contains a blank space - you may not need to quote the paths on your system.

If you are using UNIX you will, of course, use a slightly different syntax. On my UNIX system the equivalent paths are, for the includable functions

/local/omnimark53/xin/

and for the dynamic linked libraries

/local/omnimark53/lib/

To use these libraries and functions, you need to tell OmniMark where they are on your system. The most convenient way to do this is to use the '-i' and the '-x' option in an OmniMark 'args' file. The sample args file below specifies these paths and also specifies the name of a program to run on my personal computer:

-i "C:\Program Files\OmniMark\xin\"
-x "C:\Program Files\OmniMark\lib\=L.dll"
-sb myprogram.xom

The suffix '=L.dll' is optional and reminds OmniMark that, on my system dynamic linked library files have an extension of 'dll'. On my UNIX system, the equivalent 'args' file would be

-i /local/omnimark53/xin/
-x /local/omnimark53/lib/=L.so
-sb myprogram.xom

Here the suffix '=L.so' reminds OmniMark that dynamic linked library file on UNIX have the extension 'so'.

5.6.3 The 'omdate' library

The 'omdate' library contains many functions for working with dates. The program below shows the most fundamental of these. It displays the current date and time on my system.

[Code Sample: C05T06a.xom]

001  ; Using the date library
002  
003  include "omdate.xin"
004  
005  process
006    output now-as-ymdhms

To execute this program, I create a file called 'omargs':

-i "C:\Program Files\OmniMark\xin\"
-x "C:\Program Files\OmniMark\lib\=L.dll"
-sb C05T06a.xom

and call OmniMark with

omnimark -f omargs

In the same way, I can calculate the day of the week for any date and display the name of that day. This program displays the name of the day of the week for the 28th of June, 2000.

[Code Sample: C05T06b.xom]

001  ; Using the date library
002  
003  global stream weekday-name size 7 initial {
004         "Monday", "Tuesday",  "Wednesday",
005         "Thursday", "Friday", "Saturday",
006         "Sunday"}
007  
008  include "omdate.xin"
009  
010  process
011    local counter dayNum
012    local stream dayName
013  
014    set dayNum to ymd-weekday of "20000628"
015    output "It was day number %d(dayNum)%n"
016  
017    set dayName to weekday-name item dayNum
018    output "Which was a %g(dayName)%n"

For any of the other useful date functions in the 'omdate' library browse the file 'omdate.xin' which is in the 'xin' subdirectory on your system.

5.6.4 The 'omfsys' library

For file system functions, use the library 'omfsys.xin'. It contains utilities which allow you to make directories, change file permissions, delete files, list the files in a directory, rename files and so on. The sample program below uses the 'FS_ListDirectory' library function to capture the names of all the files in the current working directory ('"."').

[Code Sample: C05T06c.xom]

001  ; Using the file system library
002  
003  global stream fileNames variable initial-size 0
004  global stream errorMessage
005  
006  include "omfsys.xin"
007  
008  process
009    FS_ListDirectory "." into fileNames status errorMessage
010    repeat over fileNames
011      output fileNames join "%n"
012    again
013    output "The status of the operation was %n%g(errorMessage)%n"

For any of the other useful file system functions, browse the file 'omfsys.xin' which is in the 'xin' subdirectory on your system.

5.6.5 The 'omfloat' library

The counter data type which was introduced in Chapter 2, Topic 6 can only do calculations with whole numbers (integers). For applications requiring floating point calculations the 'omfloat.xin' library can be used. This library introduces the new data type 'float' for your programs to use. Once you include the library, you can then declare float variables and used them in the included functions. This program shows just the bare minimum of what is required. It creates two float variables, assigns values to them using literal strings, subtracts the values, and displays the result with four decimal places.

[Code Sample: C05T06d.xom]

001  ; Using 'float' variables
002  
003  include "omfloat.xin"
004  
005  global float num1
006  global float num2
007  global float difference
008  global stream result
009  
010  process
011    set num1 to FP_v "5.675"
012    set num2 to FP_v "3.671"
013    set difference to FP_sub( num1, num2 )
014    set result to FP_d( difference, 4 )
015    output "The difference is %g(result)%n"

For any of the other useful floating point functions browse the file 'omfloat.xin' which is in the 'xin' subdirectory on your system.

5.6.6 OmniMark standard library principles

As a summary on the use of OmniMark included libraries, follow these steps:

Topic List


Tasks

Task 1

A file called 'stocks.dat' contains on each line a three letter stock exchange code, a colon and a company name. An example from the file is shown here:

CML:Coles Myer
DVT: DavNet
BHP:Broken Hill Pty Ltd
ASX:   Australian Stock Exchange
JLA: Julia Mines
RKN:  Reckon Pty Ltd
NAB:National Australia Bank

Write a program which reads this file and captures the data into a stream shelf. The value of each shelf cell should be the company name and the key for each cell should be the three letter code. Your program should display all the shelf values.

Task 2

Modify the program from the previous task so that the company names are displayed in alphabetical order. Use the 'insertToStream' function.

Task 3

Using the same input file, capture all the exchange codes into a single stream with a comma between each code and all the company names into another stream with a comma between each. The company names should have quotation marks around them. Display the two streams each on a separate line of output. The output should appear something like this:

CML,DVT,ASX,JLA,RKN,NAB,...
"Coles Myer", "DavNet", "Australian Stock Exchange", ...

Hint: open each stream as a buffer and 'put' data onto the buffers as it is found. If possible, try to avoid having a trailing comma at the end of each stream.

Task 4

The file 'quiz.dtd', shown here, defines the structure for SGML instances defining multiple choice questions.

<!-- preliminary DTD for Oweek quiz -->

<!ELEMENT quiz - o (question+)>
<!ELEMENT question - o (text, altlist, hint)>
<!ATTLIST question id ID #REQUIRED
                   author CDATA #REQUIRED
                   unit CDATA #REQUIRED
                   answer NUMBER #REQUIRED>

<!ELEMENT text - o (para+)>
<!ELEMENT altlist - o (alt+)>
<!ELEMENT (para,alt) - o (#PCDATA)>
<!ATTLIST alt id NUMBER #REQUIRED>
<!ELEMENT hint - o (#PCDATA)>

An SGML instance file 'quiz.sgml', shown below, contains some multiple choice questions conforming to the above DTD.

<!DOCTYPE quiz SYSTEM "quiz.dtd">

<QUIZ>
<QUESTION
  id=q1
  author="Errol Chopping"
  unit="School of Information Technology"
  answer=1>
<TEXT>
<para>What is the name of the mountain on which Bathurst hosts
motor racing events?
<ALTLIST>
<ALT id=1>Mount Panorama
<ALT id=2>Mount Fuji
<ALT id=3>Clover Mountain
<ALT id=4>Rev-head Mountain
<HINT>Look west from the CSU campus to see the name written on
the hillside.

<QUESTION
  id=q8
  author="Ruth Radliff"
  unit="School of Information Services"
  answer=3>
<TEXT>
<para>What is the URL of Charles Sturt University's main web site?
<ALTLIST>
<ALT id=1>cblake@csu.edu.au
<ALT id=2>csu.com
<ALT id=3>www.csu.edu.au
<ALT id=4>www.csu.gov.au
<HINT>CSU is an educational institution in Australia

<QUESTION
  id=q3
  author="Frank Jones"
  unit="Division of Cultural Risk"
  answer=2>
<TEXT>
<para>The generic name for the Bathurst campus of CSU is...
<ALTLIST>
<ALT id=1>Macquarie
<ALT id=2>Mitchell
<ALT id=3>Riverina
<ALT id=4>Murray
<HINT>The name is the same as the highway from Bathurst to Orange.

For this task, write a program which scans the SGML file and writes an HTML file containing the questions. Do not indicate the correct answer for each question, nor the hint for each question in the HTML file. Questions should appear numbered consecutively as they are output (do not use the 'id' attribute for numbering).

Task 5

Using the same file 'quiz.sgml' as input, produce an HTML report which lists the questions. The report should appear in an output file called 'answers.html' and should be positioned in a table with each row of the table holding one question. In any row, the cells should contain the question id, its text and its answer respectively.

Task 6

Modify the report table program from the previous task so that the number of questions appears before the actual table.


Sample Solutions

Solution 1

A program which reads stock exchange codes and company names into a shelf:

[Code Sample: C05S01.xom]

001  ; Displaying Stock Name
002  
003  global stream stock variable initial-size 0
004  
005  process
006    submit file "stocks.dat"
007    repeat over stock
008      output stock join "%n"
009    again
010  
011  find line-start uc{3} => code
012       ":" white-space*
013       any-text+ => coname
014    set new stock key code to coname
015  
016  find any

Solution 2

A modification of the program above which adds the company names to an ordered shelf.

[Code Sample: C05S02.xom]

001  ; Displaying Stock Names in order
002  
003  define function insertToStream
004     ( MODIFIABLE stream data,
005       value stream new-key,
006       value stream new-data )
007  as
008    local stream pos-key
009  
010    ;; check for empty shelf
011    do when number of data = 0
012      set new data key new-key to new-data
013      return
014    done
015  
016  
017    ;; check for append new data
018    do when new-data > data lastmost
019      set new data key new-key to new-data
020      return
021    done
022  
023  
024    ;; general case, search for location to insert
025    repeat over data
026     set pos-key to key of data
027     exit when new-data < data
028    again
029    set new data key new-key before key pos-key to new-data
030  
031  ; ----- Function ends here
032  
033  global stream stock variable initial-size 0
034  
035  process
036    submit file "stocks.dat"
037    repeat over stock
038      output stock join "%n"
039    again
040  
041  find line-start uc{3} => code
042       ":" white-space*
043       any-text+ => coname
044    insertToStream( stock, code, coname )
045  
046  find any

Solution 3

The 'streamStarted' switch in this program only becomes true after the first values are put onto the buffers. When this switch is true, a comma is put onto each buffer before the company code and data.

[Code Sample: C05S03.xom]

001  ; Two buffered streams
002  
003  global stream codeStream
004  global stream nameStream
005  global switch streamStarted initial {false}
006  
007  process
008    open codeStream as buffer
009    open nameStream as buffer
010    submit file "stocks.dat"
011    close codeStream
012    close nameStream
013    output codeStream join "%n" join nameStream join "%n"
014  
015  find line-start uc{3} => code
016       ":" white-space*
017       any-text+ => coname
018    do when streamStarted
019      put codeStream "," join code
020      put nameStream ",%"" join coname join "%""
021    else
022      put codeStream code
023      put nameStream "%"" join coname join "%""
024      activate streamStarted
025    done
026  
027  find any

Solution 4

A program to render the SGML instance 'quiz.sgml' into HTML

[Code Sample: C05S04.xom]

001  ; Convert SGML multiple choice
002  ; to HTML.
003  
004  process
005    local stream htmlfile
006    open htmlfile as file "quiz.html"
007    do sgml-parse document
008      scan file "quiz.sgml"
009      using output as htmlfile
010        output "%c"
011    done
012    close htmlfile
013  
014  global counter questionNum initial {0}
015  
016  element quiz
017    output "<HTML>%n<HEAD>%n"
018    output "<TITLE>Quiz Web Page</TITLE>%n"
019    output "</HEAD><BODY>%n"
020    output "<CENTER><H2>Quiz Web Page</H2></CENTER>%n"
021    output "<HR>%n"
022    output "%c"
023    output "<HR>%n"
024    output "</BODY>%n</HTML>%n%n"
025  
026  element question
027    increment questionNum
028    output "<H3>Question %d(questionNum)</H3>%n"
029    output "%c"
030  
031  element text
032    output "%c"
033  
034  element para
035    output "<P>%c</P>%n"
036  
037  element altlist
038    output "<OL type='a'>%n"
039    output "%c%n"
040    output "</OL>%n"
041  
042  element alt
043    output "<LI>%c%n"
044  
045  element #implied 
046    suppress

A screenshot of the resulting file 'quiz.html' is shown below:

Solution 5

The following program produces the multiple choice questions, ids and answers in an HTML table.

[Code Sample: C05S05.xom]

001  ; An HTML report for a quiz
002  
003  process
004    local stream htmlfile
005    open htmlfile as file "answers.html"
006    do sgml-parse document
007      scan file "quiz.sgml"
008      using output as htmlfile
009        output "%c"
010    done
011    close htmlfile
012  
013  global stream correctAlt
014  
015  element quiz
016    output "<HTML>%n<HEAD>%n"
017    output "<TITLE>Quiz Report</TITLE>%n"
018    output "</HEAD><BODY>%n"
019    output "<H2>Quiz Report</H2>%n"
020    output "<TABLE BORDER>%n"
021    output "<TR><TD><STRONG>Question id</STRONG>%n"
022    output "<TD><STRONG>Question Text</STRONG>%n"
023    output "<TD><STRONG>Answer</STRONG>%n"
024    output "%c"
025    output "</TABLE>%n"
026    output "</BODY>%n</HTML>%n%n"
027  
028  element question
029    output "<TR><TD align=center>%v(id)%n"
030    set correctAlt to attribute answer
031    output "%c"
032  
033  element text
034    output "<TD>%c"
035  
036  element para
037    output "<P>%c</P>%n"
038  
039  element altlist
040    output "%c"
041  
042  element alt
043    do when "%v(id)" = "%g(correctAlt)"
044      output "<TD>%c%n"
045    else
046      suppress
047    done
048  
049  element #implied 
050    suppress

The report, when viewed with a web browser, appears thus:

Solution 6

By using a referent for the number of questions, we can have this appear before the questions are processed. A program which does this is:

[Code Sample: C05S06.xom]

001  ; An HTML report for a quiz
002  ; with number of questions.
003  
004  process
005    local stream htmlfile
006    open htmlfile with referents-allowed as file "answers.html"
007    do sgml-parse document
008      scan file "quiz.sgml"
009      using output as htmlfile
010        output "%c"
011    done
012    close htmlfile
013  
014  global counter QuestionCount initial {0}
015  global stream correctAlt
016  
017  element quiz
018    output "<HTML>%n<HEAD>%n"
019    output "<TITLE>Quiz Report</TITLE>%n"
020    output "</HEAD><BODY>%n"
021    output "<H2>Quiz Report</H2>%n"
022    output "<HR>%n"
023    output "This table contains "
024    output referent "NC"
025    output " questions.%n"
026    output "<HR>%n"
027    output "<TABLE BORDER>%n"
028    output "<TR><TD><STRONG>Question id</STRONG>%n"
029    output "<TD><STRONG>Question Text</STRONG>%n"
030    output "<TD><STRONG>Answer</STRONG>%n"
031    output "%c"
032    output "</TABLE>%n"
033    output "</BODY>%n</HTML>%n%n"
034    set referent "NC" to "%d(QuestionCount)"
035  
036  element question
037    increment QuestionCount
038    output "<TR><TD align=center>%v(id)%n"
039    set correctAlt to attribute answer
040    output "%c"
041  
042  element text
043    output "<TD>%c"
044  
045  element para
046    output "<P>%c</P>%n"
047  
048  element altlist
049    output "%c"
050  
051  element alt
052    do when "%v(id)" = "%g(correctAlt)"
053      output "<TD>%c%n"
054    else
055      suppress
056    done
057  
058  element #implied 
059    suppress

The resulting page appears as follows: