3. VARIABLES 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| [&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%() are 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 to slow down the scrolling-speed, or press the -key to stop the scrolling temporarily. - scroll slowly - 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 in Fileselector) - New - Merge (press again) - Save (press 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 (page 3-7) ARRSHUFL Shuffle the numbers in a word-array: @array_shuffle(array&()) Clock (page 3-8) 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 (page 3-9) DATE_NEW Enter new system-date at current cursor-position: PRINT "Enter new system-"; @date_new Date_print (page 3-8) 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 (page 19) 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# seconds 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 (page 3-9) 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 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 (page 3-8) 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$