Your first Newton program in NS BASIC, a closer look


This month, I'm going talk about some of the issues I glossed over before. This column will give you some of the "foundation" that I skipped last time, in my rush to get a program in your hands. For the absolute beginner, this will hopefully help shed light on last month's program. For the rest of you, it might be worth a quick skim, even if you're comfortable with these topics. The Undocumented Tip this month is very cool, but relies on using some NewtonScript functions. Even if you don't know (or care to know) NewtonScript, there are at least two reasons to have a look at it. Since the mail-bag has exactly zero entries, there does not seem to be many burning questions from you, the reader. I'll keep my eMail door open (js12@gte.com) for any NS BASIC questions or suggestions.

BIG NEWS

NS BASIC Corporation has released a new version (2.5) that really does provide a decent (almost twice as fast in some operations) speed improvement. If you bought NS BASIC, be sure to send in your registration to receive the current version!

NS BASIC Column Recap

So far, I've talked about line numbers, the PRINT and INPUT statements, and variable names. You use line numbers to indicate the order that the program statements are executed. You use PRINT to display information in the NS BASIC text display window. You use INPUT to read one or more values from the user, via the on-screen keyboard. Variable names can be any length, and are not case sensitive. Variables may hold any type of data, unless they end in a $. If this is the case, then they can only hold a string value, and assigning a number to one of these values will cause that number to be converted into it's string equivalent.

Data Types

Last month I presented a program that prints out a loan amortization schedule. That program used three data types: integers, reals, and strings. What's a data type? It's a way of describing the general behavior of a chunk of data. That may be less than helpful, so let's try an example. If you need to calculate some value that will always be a whole number, then you are using an integer data type. Common uses of integers include counters (as in our month and years counters in the loan program.) I you want to represent fractional values, then you will need to use the real data type. A common use of reals is in monetary calculations (as in our monthly payment computation in the loan program.) There are other data types available in NS BASIC. One of the most commonly used types is the string. A string is a sequence on characters enclosed in quotation marks. For example, "this is a string!" You will use strings to display information in PRINT statements, and to INPUT information. Strings are the way use get text into and out of a program.

Containers

There are two data types that are used as containers. Their only function is to hold multiple values in one place. You can think of them as providing a single variable name for a set of related values.

Arrays let you hold a specific number of values in one variable, and access those values by supplying a number. The number is called the array index. It's a lot like having a group of numbered bins. Each bin can hold any type of data, even another array or frame. You must create an array before you start storing items into the individual bins. There's more than one way to create an array, and once you've got one you can make it have more bins, fewer bins, and find out just how many bins it has.

Frames also let you hold multiple values in one variable. Unlike arrays, you must give each value within a frame a name. The name is called the field name, and looks a lot like a variable name. Frames can have any number of fields, and each field can hold any type of data, including another frame or array. There's more than one way to create a frame, and once you've got one you can make it have more fields, fewer fields, and find out just how many fields it has, along with their names. Frames are very important, because you use them to store and read information from files.

I'll present some example programs showing Array and Frame usage in a future column.

Booleans

The Boolean data type is unique, because it has only two values: true and false. In a wild fit of impishness, the Newton world calls false nil. So in NS BASIC, a boolean can be true or nil. It's like a switch, either on (true) or off (nil). It turns out that this is really useful, because when you start asking simple yes/no questions, you can actually find out quite a bit about something. Remember the 20 questions game? You'll find that the same technique (asking questions and then doing different things based on a true/nil answer) is used quite a bit in programming. There are ways to ask several true/nil questions at once. The boolean operators AND, OR, and NOT can be used to string several together. Remember those truth tables from the seventh grade? Well now you can finally use that knowledge!

Variables and data types

In general, you can just start using a variable name in your program by assigning some information to it. Unless you use a variable ending in $, you can assign any data you'd like to it. Any old information currently stored in the variable is discarded.

Flow Control

We're not talking about plumbing... well not exactly anyway. Actually, on second thought, let's look into plumbing for just a minute. You house has a "program" for delivering fresh water and removing dirty water. Since all the fresh water comes from one place, the plumbing branches out from this one point to several locations where it is needed. On the return trip, the branches connect the different locations until you end up with just one outflow pipe. Along the way, there are different valves that can be used to shut off water to specific sections. If you were really bored, you could actually trace a single drop of water as it flowed through your plumbing. So how is this like a program? Good question. You program is executed one line at a time. Execution starts at the top, the lowest numbered program line (unless you do something specifically to tell NS BASIC where to start, which is another column entirely!) Execution is like a single drop of water, it will go through exactly one path of your program at a time. If you don't use any of the flow control statements, it will start at the top and continue executing each line, exactly once, until it reaches the last line.

NS BASIC has a number of statements that let you alter the flow of statement execution. The most basic of these (sorry for the pun!) is the GOTO statement. A GOTO statement specifies the next line of the program that is to be executed. That statement is executed next, and then execution proceeds forward from that point. If the GOTO statement specifies a line number lower than the current line, a loop is formed. If no other flow control statements are contained in this loop, your Newton will be locked into an infinite loop, and you'll need to use the reset button to stop the program. This simple program will cause your Newton to print "Help! I'm stuck in a loop" until you press the reset button:

10 REM infinite loop!
20 PRINT "Help! I'm stuck in a loop"
30 GOTO 20

Since you never gave the program a way to get out of the loop, it just goes around and around forever! The IF THEN ELSE statement can be used to test for a condition and perform one statement if it is true or another statement if it is nil. I'll use an IF THEN ELSE to give our program a way to get out of the loop:

10 REM not so infinite loop!
20 PRINT "Help! I'm stuck in a loop"
30 PRINT "Do you want me to stop (Y/N)"
40 INPUT pleaseStop$
50 IF pleaseStop$ = "Y" THEN END ELSE GOTO 20

Line 50 tests to see if the user enters a Y. If they do, the THEN portion is executed, and the program ends. If they don't, the ELSE portion is executed and the program branches back to the top of the loop.

Since loops are very common, NS BASIC has a pair of statements that are used to execute a block of code a number of times. The FOR/NEXT statement pair specifies a block of code that is to be executed a number of times. The FOR statement sets up a loop variable that will be set to a sequence of numbers. You specify the name of the variable, its initial value, its final value, and the amount to add to it each time you complete a repetition of the loop. Here's a simple program that prints a series of numbers:

10 REM series FOR/NEXT loop
20 FOR i = 0 to 10 STEP 2
30 PRINT i
40 NEXT i

This prints 0, 2, 4, 6, 8, 10. You may use integers or reals in a FOR/NEXT loop, and the STEP may be positive (to count up) or negative (to count down.) If you omit STEP, it is 1.

Let's have another look at our LOAN.BAS program from last month to see these techniques in action:

10 REM LOAN.BAS
20 PRINT "Enter amount borrowed:"
30 INPUT borrowed
40 PRINT "Enter interest rate (enter 8 for 8%):"
50 INPUT baseRate
55 monthlyRate = baseRate/1200
60 PRINT "Enter number of years:"
70 INPUT years
80 REM Calculate monthly payment
90 mpay = borrowed/ANNUITY(monthlyRate, years*12)
100 PRINT
110 PRINT "Monthly Payment: $"; FORMATTEDNUMBERSTR(mpay,"%.2f")
120 PRINT
130 PRINT "Pmt Bal Prin Int"
140 pmt = 1
150 FOR i = 1 to 12
160 interest = borrowed*monthlyRate
170 principal = mpay-interest
180 PRINT pmt; FORMATTEDNUMBERSTR(borrowed,"%11.2f"); FORMATTEDNUMBERSTR(principal,"%11.2f"); FORMATTEDNUMBERSTR(interest,"%11.2f")
190 borrowed = borrowed - principal
200 pmt = pmt + 1
210 NEXT i
240 IF pmt >= years*12 THEN END
250 PRINT "Enter Q to Quit, return to continue:"
260 INPUT more$
270 IF more$ <> "Q" THEN GOTO 150
280 END

Here we see a FOR/NEXT loop in lines 150-210 to compute a single year (12 payments) of the loan. A larger enclosing loop using GOTO and IF THEN ELSE is from lines 150-270. This loop executes the inner loop as many times as needed for the number of years for the loan (line 240), and to pause the output after each year and let the user quit (lines 250-270.) Note that we could not use a FOR/NEXT loop using pmt as the outer loop, because we need to increment pmt ourselves within the inner loop.

The GOSUB/RETURN statement pair is used to call an return from a subroutine. A subroutine is just a block of code that performs a well defined operation. It is often used more than once from several locations in a program, and is used to make a program more compact and modular. Instead of repeating the same chunk of code every time it is needed, you need only write it once and call it when needed. I'll go into subroutines and the GOSUB statement in another column.

There are also some special forms of GOTO and GOSUB that can be used to create multiple branches in a single statement. These forms (often called computed GOTO and computed GOSUB) let you select one particular line number to GOTO or GOSUB based on the value of a variable or expression. The ON ... GOTO statement selects one line number from a set of line numbers, based on the value of an expression. Here's an example:

10 ON ... GOTO example
20 PRINT "Enter a number between 1 and 5"
30 INPUT aNumber
40 ON aNumber GOTO 100, 200, 300, 400, 500
50 PRINT "You entered a number outside of the range!
60 END
100 PRINT "One"
110 END
200 PRINT "Two"
210 END
300 PRINT "Three"
310 END
400 PRINT "Four"
410 END
500 PRINT "Five"
510 END

The ON ... GOSUB statement works in much the same way, except the selected line is called as a subroutine. Notice in the example program that if the value of the expression is outside the range, the next line is executed instead.

Next Month

I'll introduce file input and output in NS BASIC. You can use files to save data from a program, and retrieve it at a later time. In other words, your data can live on after your program ends. You can also access the built-in data stored by Names, Notes, Dates, and To-Do. I'll show you how to get started.

Undocumented Tip

You seasoned NS BASIC programmers are probably itching to get better performance out of files. Did you know that you can bypass the file statements (OPEN, GET, PUT, and the new CLOSE [2.5]) and use NewtonScript functions directly to get a bit of a boost? You can even specify powerful and fast searches using cursors, and add and drop indexes! That's right, your files can have more than one index field!

Using NewtonScript Soups

You should always use the CREATE statement to create new files. You can open an existing file using the NewtonScript function GetUnionSoup(). Here's how:

theSoup := GetUnionSoup("MySoup")

Note the use of the alternate assignment statement :=. You must use this form for all soup related functions. If the file does not exist, the value of theSoup will be nil. If the value is nil, then you should create the file using the CREATE statement, and then use GetUnionSoup again. Once you've done this, you can use AddToDefaultStore() to add new frames to the file:

addedOk := U.theSoup:AddToDefaultStore(theFrame)

Note the use of U. to access the special methods of the union soup. addedOk will be true if the frame was added, or nil if it was not. Use this to add new frames to a file instead of the PUT statement.

Faster File Searching

You can search the file using the Query() function. A sequential search of the records, in the order they were added, is accomplished using:

cursor := Query(theSoup, {type: 'index})

You can now access all the records in the file directly, using the following functions:

entry := U.cursor.Current // the current entry
entry := U.cursor:Next() // the next entry, or nil if the end was reached
entry := U.cursor:Prev() // the previous entry, or nil if the end was reached
entry := U.cursor:Move(n) // the nth entry from the current, or nil if the end was reached
entry := U.cursor:GotoKey(k) // the entry with the index value >= k

You can reset the cursor to the first value using:

unUsed := U.cursor:Reset()

Unlike GET, which can only find a key value or the next frame, you can freely move forwards and backwards in the file. If you specify the index field in the Query() statement, you can access and search based on the key value. In our example suppose we have a file with a key field of name. This Query() accesses the file in name key order:

cursor := Query(theSoup, {type: 'index, indexPath: 'name})

Modifing and Deleting Frames

When using cursors, you can modify or delete the frames retrieved. Let's assume that the variable entry has a frame in it from the cursor. Use the following expressions to update or delete the frame:

unUsed := EntryChange(entry) // update the changes (like PUT)
unUsed := EntryRemoveFromSoup(entry) // delete the frame (like DEL)
theSize := EntrySize(entry) // computes storage space used by entry

Adding and Droping Indexes

You can add additional indexes to a file using AddIndex():

unUsed := U.theSoup:AddIndex({structure: 'slot, path: 'field, type: 'string}) // add index on field containing strings

If we wanted to add an index to our file on an age field, we'd use:

unUsed := U.theSoup:AddIndex({structure: 'slot, path: 'age, type: 'int})

Where path is the name of the field to index, and type is either 'string, 'int, or 'real. You can also delete indexes using RemoveIndex():

unUsed := U.theSoup:RemoveIndex('age)

I Want To Know More!

You can do very powerful searches using indexes. You can search for all frames that contain a specific word or string, for example. Consult one of the NewtonScript books ("Programming for the Newton" or "Wireless for the Newton") for examples of using soups and cursors.

The following program creates a data file, populates it with some frames, adds a second index, and then accesses the records using the two indexes. It then does a text search. When it is completed it removes the file:

10 REM soups & cursors
15 PRINT "Opening or creating the file..."
20 theSoup := GetUnionSoup("TestSoup:JCSEAO")
30 IF theSoup THEN GOTO 100
40 CREATE ch, "TestSoup:JCSEAO", name
50 GOTO 20
100 REM add some frames
105 PRINT "Adding some frames..."
110 theFrame := {name: "Smith, John", age: 30}
120 addedOk := U.theSoup:AddToDefaultStore(theFrame)
130 theFrame := {name: "Smith, Jane", age: 20}
140 addedOk := U.theSoup:AddToDefaultStore(theFrame)
150 theFrame := {name: "Doe, Jane", age: 18}
160 addedOk := U.theSoup:AddToDefaultStore(theFrame)
170 theFrame := {name: "Doe, John", age: 16}
180 addedOk := U.theSoup:AddToDefaultStore(theFrame)
200 REM create second index on age
205 PRINT "Adding Index on age..."
210 unUsed := U.theSoup:AddIndex({structure: 'slot, path: 'age, type: 'int})
300 REM access frames in order added
305 PRINT "Access in order added..."
310 cursor := Query(theSoup, {type: 'index})
320 IF NOT cursor.current THEN GOTO 400 // hit EOF
330 PRINT cursor.current
340 entry := U.cursor:Next() // the next entry, or nil if the end was reached
350 GOTO 320
400 REM access in name order
405 PRINT "Access in name order..."
410 cursor := Query(theSoup, {type: 'index, indexPath: 'name})
420 IF NOT cursor.current THEN GOTO 500 // hit EOF
430 PRINT cursor.current
440 entry := U.cursor:Next() // the next entry, or nil if the end was reached
450 GOTO 420
500 REM access in name order
505 PRINT "Access in age order..."
510 cursor := Query(theSoup, {type: 'index, indexPath: 'age})
520 IF NOT cursor.current THEN GOTO 600 // hit EOF
530 PRINT cursor.current
540 entry := U.cursor:Next() // the next entry, or nil if the end was reached
550 GOTO 520
600 REM Text search
605 PRINT "Accessing all frames with John in them..."
610 cursor := Query(theSoup, {type: 'text, text: "John"})
620 IF NOT cursor.current THEN GOTO 700 // hit EOF
630 PRINT cursor.current
640 entry := U.cursor:Next() // the next entry, or nil if the end was reached
650 GOTO 620
700 REM clean up
710 DELETE "TestSoup:JCSEAO"

Here's a sample run:

* run
Opening or creating the file...
Adding some frames...
Adding Index on age...
Access in order added...
{name:"Smith, John",age:30,_uniqueID:0}
{name:"Smith, Jane",age:20,_uniqueID:1}
{name:"Doe, Jane",age:18,_uniqueID:2}
{name:"Doe, John",age:16,_uniqueID:3}
Access in name order...
{name:"Doe, Jane",age:18,_uniqueID:2}
{name:"Doe, John",age:16,_uniqueID:3}
{name:"Smith, Jane",age:20,_uniqueID:1}
{name:"Smith, John",age:30,_uniqueID:0}
Access in age order...
{name:"Doe, John",age:16,_uniqueID:3}
{name:"Doe, Jane",age:18,_uniqueID:2}
{name:"Smith, Jane",age:20,_uniqueID:1}
{name:"Smith, John",age:30,_uniqueID:0}
Accessing all frames with John in them...
{name:"Smith, John",age:30,_uniqueID:0}
{name:"Doe, John",age:16,_uniqueID:3}

Due to undesirable interactions between NS BASIC's CREATE and DELETE statements and the NewtonScript soup and cursor functions, you'll need to exit NS BASIC after you run this program in order for it to run successfully again. This is a problem only when you try to create and delete a file, as well as use the NewtonScript functions, in the same program. I'm looking into alternatives to using CREATE and DELETE, and I'll provide them in a follow-up column.