Skip to main content
© Bizkid

 "A  Japanse  person  goes to a  junkfood  restaurant,  and  asks
'Hakkie tippo wawa kili Coca Cola?'
 The restaurant owner reacts: 'You want a can of WHAT?'"


                YOUR SECOND GFA BASIC 3.XX MANUAL
                             - or -
        HOW I LEARNED TO STOP WORRYING AND LOVE GFA-BASIC
                             PART 4
                    CHAPTER THREE - VARIABLES
                          by Han Kempen

Variable-type

 In GFA-Basic we use the expressions 'byte', 'word' and 'integer'
for  the three integer-variables (postfixes '|',  '&'  and  '%').
Because all three are integers,  the expression 'integer' for the
4-byte integer-variable is sligthly confusing. In other languages
this variable is called 'longword' or simply 'long'.  An  address
in RAM should always be a 4-byte integer. A nibble is half a byte
(4 bits; in hexadecimal notation each digit represents a nibble),
but there is no corresponding variable-type.

DEFWRD

 I  prefer  to  declare word-variables as  the  default,  so  all
variables without postfix are 2-byte word-variables:

     DEFWRD "a-z"

 IMPORTANT:  if  a number-variable has no postfix in the  Manual,
you  should  assume it's a word-variable.  Please note  that  the
interpreter  assumes  a  number-variable  without  postfix  is  a
floating point variable, unless you use DEFWRD "a-z".

 I  strongly recommend the use of word-variables  (2  bytes).  In
calculations, the use of the special integer-operators (ADD, SUB,
INC,  etc.) speeds the program up considerably. Calculations with
word-integers in a compiled program are usually faster than other
calculations.

 If you insist on using the regular operators (+,  -,  etc.)  you
should use floating point variables instead of integer-variables.
Using  the regular operators,  the interpreter converts  integer-
variables  to floating point,  does the calculation and  converts
the result back to integer again:

     a&=b&*c&            ! slow (in interpreted program)
     a#=b#*c#            ! faster
     a&=MUL(b&,c&)       ! much faster

 It  takes some time to recognize what an exotic expression  like
'DIV(a,MUL(ADD(a,b),SUB(b,c)))'  means.  I  suggest you  use  the
regular  operators if the calculation-time is  not  critical.  In
loops,  the gain in calculation time really counts, so you should
use the integer-operators. That way you will learn Polish too.

 Be  careful  if you use integer-operators  with  floating  point
variables,  as a floating point number is converted to an integer
before the calculation:

     MUL x%,y#           ! x% * INT(y#)

 If  you multiply an integer with a floating point  variable  you
usually  want  the result to be converted to  integer  after  the
calculation:

     x%=x%*y#            ! INT(x% * y#)

 If  the  range  of  word-variables  (-32768  to  32767)  is  not
sufficient,  you could use 4-byte integer-variables  (-2147483648
to 2147483647) instead. Think twice before you use floating point
variables.

Boolean

 The following five lines :

     IF number>0
       test!=TRUE
     ELSE
       test!=FALSE
     ENDIF

 can be shortened to just one line :

     test!=(number>0)

 This  works,  because  the '>'-operator returns  TRUE  or  FALSE
(actually -1 or 0).

 Another little trick :

     IF i=1
       n=n*2
     ELSE IF i=2
       n=n*5
     ELSE
       n=0
     ENDIF

 This could be shortened to :

     n = n*-2*(i=1) + n*-5*(i=2)

 The example is ridiculous,  but the principle involved could  be
useful. The expressions 'i=1' and 'i=2' are either 0 (FALSE) or -
1 (TRUE).

 It is not necessary to use something like:

     IF flag!=TRUE
       (...)
     ENDIF

 You can simply use :

     IF flag!
       (...)
     ENDIF

Integer

 You can't assign 2^31 to a 4-byte integer-variable.  Although an
integer  contains  32 bits,  you can't use bit 31 (bit 0  is  the
first bit) because this bit is a flag for a negative integer. The
largest positive number you can assign to an integer-variable  is
therefore 2^31-1 (2147483647).  I could have written an  analogue
paragraph about the 2-byte word-variables, but I didn't.

Floating Point

 The range of floating point variables (postfix #) is:

     -1.0E1000 < x# < 1.0E1000

 Larger or smaller numbers can be used in calculations,  but  not
printed  with PRINT,  because the exponent may contain  not  more
than  3 digits (1.0E+1000 is displayed  as  1.0E;00).  Also,  the
editor refuses to accept numbers larger than 1.0E+1000. Try this:

     x#=1.0E+999
     PRINT x#                      ! no problem
     x#=x#*100
     PRINT x#                      ! 1.0E+1001 can't be PRINTed
     PRINT x#/100                  ! proof that x# really is
                                   ! 1.0E+1001
     y#=1.0E+1000                  ! editor accepts it, but can't
                                   ! print it
     y#=1.0E+1001                  ! editor does not accept this

 It is possible to print numbers with four digits in the exponent
with PRINT USING:

     PRINT USING "#.#^^^^^^",x#    ! 1.0E+1001 is now printed
                                   ! correctly

 You have to use six '^' in the exponent:  one for 'E',  one  for
'+' and four for the 4-digit exponent.

 The largest number that can be used seems to be 1.0E+2158:

     z#=1.0E+999*1.0E+999*1.0E160
     PRINT USING "#.#^^^^^^",z#    ! 1.0E+2158
     z#=z#*10                      ! overflow

 If your life depends on it you should not exceed the upper limit
that is officially guaranteed by GFA (probably a relic from  GFA-
Basic 2.x days):

     x#  < 3.59E+308

 The  smallest  positive number is  2.22E-308.  Smaller  positive
numbers are treated as 0.

String

 A string is stored in memory as folllows:

     b0|b1|b2|b3|b4|b5| <string> [&H0] |b6|b7|b8|b9
        descriptor    |     string     |backtrailer

 The  descriptor contains the string-address in the  bytes  b0-b3
(used  by V:) and the string-length in the bytes b4-b5  (used  by
LEN). The actual string is followed by the null-byte (CHR$(0)) if
the string-length is an odd number. If you are going to use CHAR,
a string must always end with CHR$(0),  so you must add the null-
byte:

     t$=t$+CHR$(0)

 This is the easy way,  you could also test if the  string-length
is  even  and add the null-byte only if that  is  the  case.  The
backtrailer  contains the descriptor-address in the bytes  b6-b9.
The backtrailer is used during garbage-collection:  after finding
the descriptor-address the exact position of the entire string is
known  immediately and the string can be moved to a new place  in
memory.

VAR

 If  you call a Procedure and use VAR (call  by  reference),  all
variables  and/or arrays after VAR are called  by  reference.  An
example to clarify this:

     @test(10,5,number%,array%())
     (...)
     PROCEDURE test(a,b,VAR x%,y%())
       ' now a=10 and b=5 (call by value)
       ' number% and array%() can now be used as x% and y%()
       x%=a+b                 ! global variable number% is now 15
       ARRAYFILL y%(),1       ! all elements of array%() now 1
     RETURN

 In GFA-Basic 2.x you would have to use SWAP for calling an array
by reference, but VAR makes life much easier in GFA-Basic 3.x:

     @test(*a%())             ! GFA-Basic 2.x (also possible in
                                                             3.x)
     (...)
     PROCEDURE test(ptr%)
       SWAP *ptr%,x%()        ! array a%() temporarily renamed
                                                          as x%()
       (...)                  ! do something with the array x%()
       SWAP *ptr%,x%()        ! restore pointer before leaving
                                                        Procedure
     RETURN
     '
     @test(a%())              ! GFA-Basic 3.x
     (...)
     PROCEDURE test(VAR x%())
       (...)                  ! do something with the array x%()
     RETURN

FUNCTION

 You can only leave a FUNCTION by RETURNing a value or a  string.
This  value/string  is usually assigned to  a  variable.  If  the
FUNCTION returns a string, the function-name has to end with '$':

     PRINT @test
     FUNCTION test
       RETURN 126
     ENDFUNC
     '
     PRINT @test$
     FUNCTION test$
       RETURN "this is a string"
     ENDFUNC

 You can use as many RETURNs within a FUNCTION as you like.

CLEAR

 Because CLEAR is automatically executed when you run a  program,
it's not necessary to start your program with this command.

ERASE

 It's impossible to reDIMension an existing array. You first have
to  ERASE  the existing array and then you can  DIMension  a  new
array.  It is not necessary to test for the existence of an array
with  DIM?() before you use ERASE.  In other words,  you can  use
ERASE even if the array doesn't exist:

     ERASE array$()      ! just in case this array already exists
     DIM array$(200)

 Some people think it's a sin to ERASE a non-existing  array.  If
you  have the same strong feelings,  you should ERASE the  proper
way:

     IF DIM?(array$())=0
       DIM array$(200)
     ELSE
       ERASE array$()
       DIM array$(200)
     ENDIF

 After  ERASEing an array,  GFA rearranges the remaining  arrays.
All arrays that have been DIMensioned after the deleted array are
moved  in  order to fill the gap of the deleted  array.  This  is
important if you use an address like 'V:array(0)' in your program
(see paragraph 'Storing data in RAM' in chapter 4).

DUMP

 Examine all variables in your program by typing 'DUMP' in Direct
Mode. Press <CapsLock> to slow down the scrolling-speed, or press
the <Right Shift>-key to stop the scrolling temporarily.

     <CapsLock>          - scroll slowly
     <Right Shift>       - stop scrolling (release to scroll
                           further)

 This  is  the best way to discover those nasty  typo-bugs  in  a
variable-name.  You'll probably be surprised to see the names  of
deleted variables as well.  Also,  any variable-name you used  in
Direct Mode appears.  All these names are Saved with the program!
Delete all unwanted names by temporarily saving the program as  a
LST-file:

     - Load the file
     - Save,A (press <Return> in Fileselector)
     - New
     - Merge (press <Return> again)
     - Save (press <Return> once more)

 The file could be much shorter after this operation.

TYPE-bug

 The command TYPE does not always work properly if you use  local
variables.  TYPE  sometimes returns -1 (= error) instead  of  the
proper TYPE-number.  In a compiled program TYPE always returns -1
for any global/local variable.  Buy a bottle of TYPE-Ex and erase
TYPE completely from your manual.

READ

 As a rule, I always RESTORE the appropriate label before READing
DATA-lines. That way I can use DATA-lines in Procedures:

     PROCEDURE read_data
       RESTORE these.data
       READ a,b,c,d
       these.data:
       DATA 1,2,3,4
     RETURN

SWAP

 The 52 cards in bridge (or another card-game) can be represented
by  a byte-array.  Fill the elements 0-51 of the array  with  the
value  0-51.  The  values  0-12 would  represent  the  Club-cards
(2,3,4,...,Q,K,A),  values 13-25 the Diamonds,  values 26-38  the
Hearts and values 39-51 the Spades.  Shuffling the cards can  now
be simulated as follows:

     DIM deck|(51)
     FOR i=0 TO 51
       deck|(i)=i
     NEXT i
     FOR i=DIM?(deck|())-1 DOWNTO 0
       j=RAND(i)+1
       SWAP deck|(j),deck|(i)
     NEXT i

TIME$

 You  can use TIME$ to print the current time on the  screen.  In
the  Procedure  Clock you'll find a way to print the  time  every
second, although TIME$ is updated every two seconds:

     EVERY 200 GOSUB clock
     PROCEDURE clock
       LOCAL t$
       t$=TIME$
       IF t$=clock$
         MID$(clock$,8)=SUCC(RIGHT$(clock$))
       ELSE
         clock$=t$
       ENDIF
       PRINT AT(1,1);clock$
     RETURN

 That's  pretty  clever,  although the use of a  global  variable
(clock$) in a Procedure is not to be  recommended.  Unfortunately
the clock stops completely during the execution of time-consuming
commands like PAUSE or BLOAD.

DATE$

 Find the day of the week with Zeller's Congruence:

     day=VAL(LEFT$(day.date$,2))
     mp=INSTR(day.date$,".")
     month=VAL(MID$(day.date$,mp+1,2))
     year=VAL(RIGHT$(day.date$,4))
     IF month<=2
       m=10+month
       year=year-1
     ELSE
       m=month-2
     ENDIF
     h=year/100
     y=year-100*h
     w=(TRUNC(2.6*m-0.2)+day+y+TRUNC(y/4)+TRUNC(h/4)-2*h) MOD 7
     RESTORE weekdays
     FOR n=0 TO w
       READ weekday$
     NEXT n
     '
     weekdays:
    DATA Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday

SETTIME

 You can use SETTIME to change the system-time or the system-date
or both:

     SETTIME new.time$,DATE$            ! change system-time
     SETTIME TIME$,new.date$            ! change system-date
     SETTIME new.time$,new.date$        ! change both

 Of  course you can also use DATE$ and TIME$ to change  time  and
date:

     DATE$=new.date$
     TIME$=new.time$

 The format for the time-string is "hh:mm:ss",  but you can  also
use one digit for hours and/or minutes ("1:1:00").

 The  format for the date-string is "dd.mm.yy"  or  "dd.mm.yyyy",
but you can use one digit for day and/or month ("1.1.92").

 Every  ST  has two independent clocks.  A Mega-ST  has  a  third
clock,  but  this battery-operated clock actually  overrides  the
other clocks.  The system-clock can be accessed with GEMDOS 42-45
(Tgetdate,  Tsetdate,  Tgettime and Tsettime) and is also used by
GFA-Basic.   The  keyboard-clock  is  run  independently  by  the
keyboard  and can be accessed with XBIOS 22 and 23  (Settime  and
Gettime).  If you use SETTIME or DATE$/TIME$ you use the  system-
clock  only.  After  a warm reset the keyboard-clock  is  useful,
because  it keeps running while the system-clock starts all  over
again (unless your ST has a battery-operated clock):

     d%=GEMDOS(42)*65536           ! system-date
     t%=GEMDOS(44) AND 65535       ! system-time
     ~XBIOS(22,L:d%+t%)            ! set keyboard-clock
     ~XBIOS(38,L:LPEEK(4))         ! warm reset
     x%=XBIOS(23)                  ! read keyboard-clock
     ~GEMDOS(43,x% DIV 65536)      ! restore system-date
     ~GEMDOS(45,x% AND 65535)      ! restore system-time

After  a cold reset the keyboard-clock can't be used.  You  could
save  the current date/time temporarily in a file or you'll  have
to ask the user to enter the date and time after the cold reset.

                     Procedures (CHAPTER.03)

Array_shuffle                                            ARRSHUFL
 Shuffle the numbers in a word-array:
     @array_shuffle(array&())

Clock                                                       CLOCK
 Print time every second in upper right corner of TOS-screen:
     EVERY 200 GOSUB clock

Clock_wait                                               CLCKWAIT
 Show a clock on the screen and wait until the user presses a key
or mouse-button:
     @clock_wait

Date_input and Date_input_error                          DATE_INP
 Enter date at current cursor-position:
     PRINT "Enter the date: ";     ! any format (day month year)
     @date_input(new.date$)
     PRINT "Date: ";new.date$      ! date-format "dd.mm.yyyy"
 Accepts a wide variety of date-formats and is therefore far more
flexible  than  the  Procedure  Date_new.  Uses  ERROR  to  catch
unexpected errors.

Date_new                                                 DATE_NEW
 Enter new system-date at current cursor-position:
     PRINT "Enter new system-";
     @date_new

Date_print                                               DATE_PRT
 Print a date at the current cursor position:
     PRINT "System-date is: ";
     @date_print(DATE$)
 Uses the Function Day_of_week to print something like:
     Friday 8 January 1988

Mem_test                                                 MEM_TEST
 Examine  how many bytes are used by variables  and  arrays.  Use
this number to reserve the proper amount of memory for a compiled
program (especially accessories):
     mem.free%=FRE()
     EVERY 200 GOSUB mem_test
     (...)                      ! the program you're testing

Millitimer_init and Millitimer                          MILLITIM
 Procedure uses a FOR-NEXT loop to measure time in milliseconds:
     @millitimer_init
     PRINT "Press any key ";
     PAUSE RAND(4*50)
     PRINT "NOW"
     @millitimer(ms#)
     PRINT "Your reaction time was ";ms#;" milliseconds."

Stopwatch and Stopwatch_print                            STOPWTCH
 The  Procedure  Stopwatch can be used  as,  you  guessed  it,  a
stopwatch. The elapsed time can be printed at the current cursor-
position by the Procedure Stopwatch_print:
     @stopwatch               ! start the stopwatch
     ' Do something interesting that takes some time
     @stopwatch               ! stop the stopwatch; s.seconds# s.
     PRINT "Elapsed time: ";
     @stopwatch_print         ! print elapsed time
 Time is printed as follows:
     1 h 24 m       - no seconds if ? 1 hour
     3 m 21 s       - no decimals if ? 1 minute
     34.6 s         - 1 decimal if ? 10 seconds
     6.28 s         - 2 decimals if < 10 seconds
 Of course you can change the format in Stopwatch_print,  or  you
could  use  the global variable s.seconds# from Stopwatch  to  do
your own calculations.

Time_input and Time_input_error                          TIME_INP
 Enter time at current cursor-position:
     PRINT "Enter the time: ";     ! any format (hours minutes
                                                       [seconds])
     @time_input(new.time$)
     PRINT "Time: ";new.time$      ! time-format "hh:mm:ss"
 Accepts a wide variety of time-formats and is therefore far more
flexible  than  the  Procedure  Time_new.  Uses  ERROR  to  catch
unexpected errors.

Time_new                                                 TIME_NEW
 Enter new system-time at current cursor-position:
     PRINT "Enter system-";
     @time_new(TRUE)               ! colon as separator
     '
     PRINT "Enter system-";
     @time_new(FALSE)              ! point as separator
 If  you  use  the point as separator,  you can  enter  the  time
quickly without leaving the numeric keypad.  But you have to  use
the  format  "hh.mm.ss"  (two  digits  for  hours,   minutes  and
seconds).  If you press <Return> immediately,  the system-time is
not changed.

                     Functions (CHAPTER.03)

Date_integer                                             DATE_INT
 Convert date "dd.mm.yyyy" to integer yyyymmdd to make it  easier
to compare two dates:
     IF @date_integer(d1$) > @date_integer(d2$)
       ' Date d1$ later than d2$
     ELSE
       ' Date d1$ equal to or earlier than d2$
     ENDIF

Day_of_week                                              DAY_WEEK
 Returns day of week for a date "dd.mm.yyyy":
     PRINT "System-date ";DATE$;" is a ";@day_of_week(DATE$)

Days_passed                                              DAY_PASS
 Returns number of days between two dates (format "dd.mm.yyyy"):
     d1$="1.1.1991"
     d2$="1.1.1992"
     PRINT "There are ";@days_passed(d1$,d2$);" days ";
     PRINT "between the dates ";d1$;" and ";d2$ 

Disclaimer
The text of the articles is identical to the originals like they appeared in old ST NEWS issues. Please take into consideration that the author(s) was (were) a lot younger and less responsible back then. So bad jokes, bad English, youthful arrogance, insults, bravura, over-crediting and tastelessness should be taken with at least a grain of salt. Any contact and/or payment information, as well as deadlines/release dates of any kind should be regarded as outdated. Due to the fact that these pages are not actually contained in an Atari executable here, references to scroll texts, featured demo screens and hidden articles may also be irrelevant.