Lesson 9: Text and String Manipulation

Not all games involve pressing buttons to shoot enemies on the screen. Many of the all time favorites involve entering text to play such as Hangman, Scrabble, crossword puzzles, Boggle and Text Twist just to name few. Luckily QB64 has a rich feature set of commands that allow the programmer to manipulate text entry in many different ways.

The UCASE$ and LCASE$ Statements

When a user answers a program's question, for instance, "What is the capital of Ohio?", how will the user answer? Will the user type in the correct answer? Will the answer be in UPPER or lower case or a cOmbInaTion of both? How do you as the programmer check for all of these variations? The answer lies in turning the user's response into something that can easily be checked. Type the following program in.

( This code can be found at .\tutorial\Lesson9\Ucase.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

DIM Answer$ ' the answer supplied by the user

'---------------------
'- Main Program Here -
'---------------------

PRINT
INPUT "What is the capital of Ohio? > ", Answer$ ' get response from user
PRINT
IF UCASE$(Answer$) = "COLUMBUS" THEN '             convert to upper case and test for correct answer
   PRINT "Correct!" '                              answer was correct
ELSE '                                             something other than correct answer typed in
   PRINT "You lose!" '                             user wins nothing
END IF

As long as the user knows how to spell Columbus the case of the response does not matter. The UCASE$ statement in line 14 returns the string parameter associated with it in all UPPER CASE. The LCASE$ statement does just the opposite, returning the string parameter associated with it in all lower case.  Line 14 could have just as easily been:

IF LCASE$(Answer$) = "columbus" THEN '             convert to lower case and test for correct answer

and the program would behave just as it did using UCASE$.

The LTRIM$, RTRIM$, and _TRIM$ Statements

User's do funny things like not enter responses as we programmers expect. You always have to take into account the different ways a response can be entered in the WRONG way by a user for your code to be effective. The example code above will fail if the user enter spaces (presses the space bar) either before or after the word Columbus. The spaces will be seen as legitimate characters typed in by the user.

There are three statements that will remove leading, trailing, and both leading and trailing spaces from a string. LTRIM$ removes leading, or left hand, spaces from a string. RTRIM$ removes trailing, or right hand, spaces from a string, and _TRIM$ removes both leading and trailing spaces from a string. If you change line 14 in the example code to:

IF _TRIM$(UCASE$(Answer$)) = "COLUMBUS" THEN

Answer$ will first be converted to upper case and then have both leading and trailing spaces removed from it. Yes, this new line of code looks a bit complicated because one statement is embedded into another. This is a very common thing to do in programming that enables multiple actions to be taken on a parameter at the same time. Simply follow the order of operations to see how the line of code operates. UCASE$ falls inside of _TRIM$'s parenthesis so the UCASE$ statement is acted upon first. Once UCASE$ has returned the upper case form of Answer$ it's _TRIM$'s turn to take that upper case Answer$ and remove the leading and trailing spaces. You now have a string returned that is in both upper case and has had the leading and trailing spaces removed.

You could even change the line of code to read:

Answer$ = _TRIM$(UCASE$(Answer$))
IF Answer$ = "COLUMBUS" THEN

to permanently modify Answer$ by making the changes and then placing those changes back into Answer$ itself. This is handy if you need to reference Answer$ again later in the code as it will save you from having to test for upper case and leading or trailing spaces again.

The INSTR Statement

The previous examples work great for single word answers but those darn users will always test the limits of your code. What if the user were to answer "I think it's Columbus?" Technically the answer has the correct response embedded in the string the user entered but how do we see that in code?

The INSTR statement has the ability to search a base string with a defined search string and return a numeric value of where the search string was found within the base string. Once again let's modify the previous example code to see this in action. When you execute the program type your answer in as "I believe Columbus would be the answer" and see if you are correct.

( This code can be found at .\tutorial\Lesson9\Instr.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

DIM Answer$ ' the answer supplied by the user

'---------------------
'- Main Program Here -
'---------------------

PRINT
INPUT "What is the capital of Ohio? > "; Answer$ ' get response from user
PRINT
IF INSTR(UCASE$(Answer$), "COLUMBUS") THEN '       convert to upper case and search for correct answer
   PRINT "Correct!" '                              answer was correct
ELSE '                                             the correct answer was not within the string
   PRINT "You lose!" '                             user wins nothing
END IF

Figure 1: This is how Skynet got its start!

INSTR requires a string to search called the base string. In this case the base string is the upper case result of UCASE$, so the entire response entered is converted to upper case. The next parameter that INSTR requires is a string to search for called the search string. In line 14 we supplied INSTR with a search string of "COLUMBUS". If the string "COLUMBUS" is found anywhere within the user's response INSTR will return a numeric value of where it was found. If INSTR is anything other than zero then "Correct!" will get printed since we know that the IF...THEN statement will consider any numeric value other than zero as true.

There is one strange behavior of INSTR that needs to be pointed out. The following line of code will result in a value of 1 being returned.

Location% = INSTR("ABCDE", "") ' null string returns a value of 1

A search for a null string ( "" ) will always result in a positive value being returned (unless you search for a null string within a null string). This may sound like a bug but in actuality every string does contain a null string. You just need to be aware of this behavior if you start receiving a result you did not expect.

INSTR also has another trick up its sleeve. It can find multiple occurrences of the search string in the base string and report back where it finds all of them. Type in the following example to see how this works.

( This code can be found at .\tutorial\Lesson9\InstrDemo.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

DIM Position% ' each position the search string is found at
DIM Phrase$ '   the base string
DIM Search$ '   the search string
DIM NextLine% ' the print line explaining where search string found

'----------------------------
'- Main Program Begins Here -
'----------------------------

Phrase$ = "The rain in Spain falls mainly on the plain." ' create INSTR base string
Search$ = "ain" '                                          create INSTR search string
Position% = 0 '                                            reset position of string found
NextLine% = 4 '                                            first text line to print results
PRINT Phrase$ '                                            display the base string
DO '                                                       loop through the base string
    Position% = INSTR(Position% + 1, Phrase$, Search$) '   look for search string at last position
   IF Position% THEN '                                     was a match found?
       LOCATE NextLine%, 1 '                               yes, set cursor line location
        NextLine% = NextLine% + 1 '                        increment cursor line location for next time
       PRINT "Found "; CHR$(34); Search$; CHR$(34); " at position"; Position% ' print result to screen
       LOCATE 2, Position% '                               locate cursor position below search string
       PRINT CHR$(24); '                                   print an up arrow symbol where found
   END IF
LOOP UNTIL Position% = 0 '                                 leave when no more matches found

Figure 2: The weather in Spain seems nice

The INSTR statement can accept an optional position parameter as seen in line 20 of the code.

Position% = INSTR(Position% + 1, Phrase$, Search$)

Each time an instance of "ain" is found that position is recorded so the next time around Position% + 1 can be used to resume the search through the remainder of the base string. This continues on until Position% becomes zero, meaning no more matches found, and ending the loop.

The STR$ and VAL Statements

There will be times when you need to convert a string to a numeric value and a numeric value to a string. The STR$ statement is used to convert a numeric value to a string. The VAL statement is used to convert a string to a numeric value.

LINE INPUT "Enter a number between 1 and 10 > ", Number$
Value! = VAL(Number$) ' convert string to a numeric value

The above example shows a number being asked for however it's being saved in a string variable. The VAL statement is used to convert Number$ into an actual numeric value and then saved into the single type variable Value!. If the characters in Number$ are non-numeric, such as "Hello World", VAL will simply return a numeric value of zero.

INPUT "Enter a number between 1 and 10 > ", Value%
Number$ = STR$(Value%) ' convert numeric value to a string

In this example the opposite is being performed. The numeric value contained within Value% is being converted to a string and then saved into Number$. Positive numeric values converted to strings will always contain a leading space. The space is there for the possibility of a negative value that includes a minus sign. For example:

PRINT "*"; STR$(10); "*" '  * 10* printed to the screen
PRINT "*"; STR$(-10); "*" ' *-10* printed to the screen

You'll need to use either LTRIM$ or _TRIM$ to remove the leading space if you do not wish it to be present.

Number$ = LTRIM$(STR$(Value%)) ' no leading space

The LEN Statement

The LEN statement returns the number of characters contained in a string effectively reporting its length.

Phrase$ = "The rain in Spain falls mainly on the plain."
PRINT LEN(Phrase$) ' 44 is printed to the screen

The RIGHT$, LEFT$, and MID$ Statements

These statements are used to parse, or break apart, a string into smaller pieces. An example of this is when you use the TIME$ and DATE$ statements to retrieve the time and date from QB64. TIME$ delivers the time in the string form "HH:MM:SS" and DATE$ in the string form "MM-DD-YYYY". In order to get the individual hours, minutes, and seconds from TIME$, and the individual month, day, and year from DATE$ you'll need to to use the LEFT$, RIGHT$, and MID$ statements. Type the following example program into your IDE to show this.

( This code can be found at .\tutorial\Lesson9\ParseDemo.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

DIM MonthName$(12) ' array storing the names of the months
DIM Hours% '         numeric value of hour
DIM Month% '         numeric value of month
DIM Day% '           numeric value of day
DIM Year% '          numeric value of year
DIM Suffix$ '        day suffix
DIM AmPm$ '          AM or PM

'----------------------------
'- Main Program Begins Here -
'----------------------------

MonthName$(1) = "January" '                                                store the month names
MonthName$(2) = "February"
MonthName$(3) = "March"
MonthName$(4) = "April"
MonthName$(5) = "May"
MonthName$(6) = "June"
MonthName$(7) = "July"
MonthName$(8) = "August"
MonthName$(9) = "September"
MonthName$(10) = "October"
MonthName$(11) = "November"
MonthName$(12) = "December"
DO '                                                                       begin main loop
    Month% = VAL(LEFT$(DATE$, 2)) '                                        extract value of month
    Day% = VAL(MID$(DATE$, 4, 2)) '                                        extract value of day
    Year% = VAL(RIGHT$(DATE$, 4)) '                                        extract value of year
    Hours% = VAL(LEFT$(TIME$, 2)) '                                        extract value of hours
   IF Hours% > 12 THEN '                                                   military time?
        Hours% = Hours% - 12 '                                             yes, convert to civilian
        AmPm$ = "PM" '                                                     it's the afternoon
   ELSE '                                                                  no
        AmPm$ = "AM" '                                                     it's the morning
   END IF
   IF Day% = 1 OR Day% = 21 OR Day% = 31 THEN '                            one of these days?
        Suffix$ = "st," '                                                  yes, day ends in st
   ELSEIF Day% = 2 OR Day% = 22 THEN '                                     no, one of these days?
        Suffix$ = "nd," '                                                  yes, day ends in nd
   ELSEIF Day% = 3 OR Day% = 23 THEN '                                     no, one of these days?
        Suffix$ = "rd," '                                                  yes, days ends in rd
   ELSE '                                                                  no
        Suffix$ = "th," '                                                  day must end in th then
   END IF
   LOCATE 2, 2 '                                                           position cursor
    Dt$ = "The current date is " + MonthName$(Month%) '                    build new date string
    Dt$ = Dt$ + STR$(Day%) + Suffix$ + STR$(Year%) + "  "
   PRINT Dt$ '                                                             display date string
   LOCATE 4, 2 '                                                           position cursor
    Tm$ = "The current time is " + RIGHT$("0" + LTRIM$(STR$(Hours%)), 2) ' build new time string
    Tm$ = Tm$ + " Hours, " + MID$(TIME$, 4, 2) + " Minutes and "
    Tm$ = Tm$ + RIGHT$(TIME$, 2) + " Seconds " + AmPm$
   PRINT Tm$ '                                                             display time string
LOOP UNTIL INKEY$ <> "" '                                                  end loop if key pressed
SYSTEM '                                                                   return to Windows

Figure 3: Time to make the donuts!

The LEFT$ statement is used to grab a predetermined number of characters starting at the left-hand side of the string. In line 30 of the program LEFT$ is used to parse just the month portion of DATE$.

Month% = VAL(LEFT$(DATE$, 2)) ' extract value of month (parse MM from MM-DD-YYYY)

A statement within a statement within a statement! WooHoo, as I stated before this is a common occurrence in programming. Let's break it down.

DATE$ returns a string in the form of "MM-DD-YYYY". Then we plug that string into LEFT$:

LEFT$("MM-DD-YYYY", 2) ' get the first two left hand characters from MM-DD-YYYY

LEFT$ grabbed the first two characters of the string which equals the month. In our example this would be "04" or April. The VAL statement is now used to convert that string into an actual numeric value:

VAL("04") ' convert to a true numeric value

LEFT$ was also used in the same manner on line 33 to get the current hour from TIME$.

Hours% = VAL(LEFT$(TIME$, 2)) ' extract value of hours (parse HH from HH:MM:SS)

In line 31 the MID$ statement is used to parse out a string from within, or the middle, of DATE$:

Day% = VAL(MID$(DATE$, 4, 2)) ' extract value of day (parse DD from MM-DD-YYYY)

MID$ starts at position 4 of the string and then parses 2 characters starting at that position.

And finally the RIGHT$ statement is used  to parse out the remaining date from the right-hand side in line 32:

Year% = VAL(RIGHT$(DATE$, 4)) ' extract value of year (parse YYYY from MM-DD-YYYY)

The ASC and CHR$ Statements

The CHR$ statement is used to print any value from 0 to 255 that corresponds to a character within the ASCII table. Typically CHR$ is used to print characters that are not accessible through the keyboard. One example would be playing card suits that are in the ASCII table.

PRINT CHR$(3) heart symbol
PRINT CHR$(4) diamond symbol
PRINT CHR$(5) club symbol
PRINT CHR$(6) spade symbol

The ASC statement does just the opposite and returns the ASCII numeric value of a character passed to it.

PRINT ASC("A") ' 65 printed to the screen
PRINT ASC(" ") ' 32 printed to the screen

This example program shows how the ASC and CHR$ statements can be used together to identify keystrokes.

( This code can be found at .\tutorial\Lesson9\CHR_ASC.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

DIM KeyPress$ ' single key presses by user
DIM KeyValue% ' ASCII value of key pressed

'----------------------------
'- Main Program Begins Here -
'----------------------------

DO '                                                 begin main loop
   DO '                                              begin key input loop
        KeyPress$ = INKEY$ '                         get any key pressed
   LOOP UNTIL KeyPress$ <> "" '                      leave loop if key pressed
    KeyValue% = ASC(KeyPress$) '                     get ASCII value of key pressed
   IF KeyValue% < 32 THEN '                          is value less than 32?
       PRINT " Control key   "; '                    yes, this is a control character
   ELSEIF KeyValue% > 47 AND KeyValue% < 58 THEN '   no, is value between 47 and 58?
       PRINT " Numeric key   "; '                    yes, this is a numeric character
   ELSEIF KeyValue% > 64 AND KeyValue% < 91 THEN '   no, is value between 64 and 91?
       PRINT " UPPERcase key "; '                    yes, this is an upper case character
   ELSEIF KeyValue% > 96 AND KeyValue% < 123 THEN '  no, is value between 96 and 123?
       PRINT " Lowercase Key "; '                    yes, this is a lower case character
   ELSEIF KeyValue% = 32 THEN '                      no, is value 32?
       PRINT " Spacebar Key  "; '                    yes, this is a space character
   ELSE '                                            no
       PRINT " Symbol Key    "; '                    assume all others are symbol characters
   END IF
   PRINT CHR$(26); " "; CHR$(KeyValue%) '            print right arrow and character
LOOP UNTIL KeyPress$ = CHR$(27) '                    leave main loop when ESC key pressed

The ASC statement also has another trick up its sleeve that was not available with the original QuickBasic implementation. QB64 has added an optional position parameter:

code% = ASC(stringExpression$[, position%])

When the position% parameter is applied ASC acts like MID$ in that it will report the ASCII value at the supplied position within the string.

a$ = "The rain in Spain" ' test string
PRINT ASC(a$, 3) '         ASCII value of position 3 in test string (letter e)
PRINT CHR$(ASC(a$, 3)) '   print the character at position 3 in the test string (letter e)

When you need to find the ASCII value of a position within a string use the ASC statement instead of MID$. The ASC statement is much faster than the MID$ statement.

The STRING$ and SPACE$ Statements

When you need a lot of the same character in a row the STRING$ statement has you covered. Simply supply STRING$ with a numeric value and a character like so:

PRINT STRING$(80, "*") ' 80 asterisks will be printed to the screen

You can also supply STRING$ with the ASCII numeric value of a character.

PRINT STRING$(80, 42) '  80 asterisks will be printed to the screen

The STRING$  statement comes in handy when building text screen boxes by using the extended ASCII characters provided to do so. Type in the following example.

( This code can be found at .\tutorial\Lesson9\ASCIIBoxes.bas )

'----------------------------
'- Main Program Begins Here -
'----------------------------

PRINT
PRINT
" An ASCII box drawing demo"
PRINT " -------------------------"
PRINT
PRINT
CHR$(218); STRING$(17, 196); CHR$(191)
PRINT CHR$(179); " Single Line Box "; CHR$(179)
PRINT CHR$(192); STRING$(17, 196); CHR$(217)
PRINT
PRINT
CHR$(201); STRING$(17, 205); CHR$(187)
PRINT CHR$(186); " Double Line Box "; CHR$(186)
PRINT CHR$(200); STRING$(17, 205); CHR$(188)

Figure 4: Old school ASCII boxes

The SPACE$ statement is used to generate spaces of a requested length.

PRINT SPACE$(80) ' 80 spaces printed to the screen

The SWAP Statement

The SWAP statement is used to switch values between two numeric or string values. The variables to be swapped must be of the same type. The following example shows the SWAP statement in action.

( This code can be found at .\tutorial\Lesson9\SwapDemo.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

DIM Var1% ' create two integers to swap
DIM Var2%
DIM S1$ '   create two strings to swap
DIM S2$

'----------------------------
'- Main Program Begins Here -
'----------------------------

S1$ = "String 1" '          give variables values
S2$ = "String 2"
Var1% = 1
Var2% = 2
PRINT
PRINT
" Before swap"
PRINT " -----------"
PRINT
PRINT
" Var1% ="; Var1%
PRINT " Var2% ="; Var2%
PRINT " S1$   = "; S1$
PRINT " S2$   = "; S2$
SWAP S1$, S2$ '             swap the string values between variables
SWAP Var1%, Var2% '         swap the integer values between variables
PRINT
PRINT
" After swap"
PRINT " ----------"
PRINT
PRINT
" Var1% ="; Var1%
PRINT " Var2% ="; Var2%
PRINT " S1$   = "; S1$
PRINT " S2$   = "; S2$

Figure 5: Variable swapping

The LOCATE, CSRLIN, and POS Statements

The LOCATE, CSRLIN, and POS statements are used to set and retrieve the position of the text cursor on the screen. The following example code show hows to use the LOCATE command.

( This code can be found at .\tutorial\Lesson9\LocateDemo.bas )

'*
'* LOCATE demo
'*

DIM x%, y% ' current mouse pointer position
DIM Cur$ '   string containing current mouse pointer position
DIM Loc$ '   string containing the LOCATE command that would be used
DIM Blink% ' counter used to control blinking character

_MOUSEHIDE '                                                        hide the operating system mouse pointer
DO '                                                                begin main loop
   _LIMIT 60 '                                                      60 frames per second
   COLOR 7, 0 '                                                     white text with black background
   CLS '                                                            clear screen and print directions
   PRINT
   PRINT "             Use the mouse to move the cursor around on the screen."
   PRINT "    Click the left mouse button to print a smiley, right button to try again."
   PRINT "                             Press ESC to quit."
   WHILE _MOUSEINPUT: WEND '                                        get latest mouse update
    x% = _MOUSEX '                                                  get mouse pointer positions
    y% = _MOUSEY
    Cur$ = " CURSOR X = " + _TRIM$(STR$(x%)) + " Y = " + _TRIM$(STR$(y%)) + " " ' build string
   COLOR 14, 1 '                                                    bright yellow text with blue background
   LOCATE 25, (80 - LEN(Cur$)) \ 2 '                                center text at bottom of screen
   PRINT Cur$; '                                                    print current mouse position
   IF x% > 0 AND y% > 0 AND y% < 26 THEN '                          is the mouse on the program window?
       COLOR 15, 0
       LOCATE y%, x% '                                              yes, position the text cursor
       PRINT CHR$(219); '                                           print solid block as mouse cursor
   END IF
   IF _MOUSEBUTTON(1) THEN '                                        left mouse button clicked?
        Loc$ = "     LOCATE " + _TRIM$(STR$(y%)) + ", " + _TRIM$(STR$(x%)) + "     " ' build string
       COLOR 14, 1 '                                                bright yellow text with blue background
       LOCATE 25, (80 - LEN(Loc$)) \ 2 '                            center text at bottom of screen
       PRINT Loc$; '                                                print LOCATE command that would be used
       COLOR 15, 0 '                                                bright white text with black background
       DO '                                                         begin flashing smiley loop
           _LIMIT 60 '                                              keep loop FPS low to conserve CPU resources
           LOCATE y%, x% '                                          position the text cursor
           IF Blink% < 31 THEN '                                    counter less than 31?
               PRINT CHR$(1); '                                     yes, print a smiley face
           ELSE '                                                   no, counter is greater than 30
               PRINT CHR$(2); '                                     print a solid smiley face
           END IF
            Blink% = Blink% + 1 '                                   increment blink counter
           IF Blink% = 61 THEN Blink% = 0 '                         reset blink counter when needed
           WHILE _MOUSEINPUT: WEND '                                get latest mouse update
       LOOP UNTIL _MOUSEBUTTON(2) OR _KEYDOWN(27) '                 leave loop when right mouse button clicked
   END IF
LOOP UNTIL _KEYDOWN(27) '                                           leave main program loop when ESC pressed
SYSTEM '                                                            return to the operating system

Figure 6: Using LOCATE to position the text cursor

As you move the cursor around the screen it's position is printed at the bottom of the screen. When you click the left mouse button a smiley character appears where the mouse cursor was. The LOCATE statement that would be used to position the text cursor where the smiley is printed is shown at the bottom of the screen.  Did you notice something about the numbers that were displayed at the bottom? If you look closely they are reversed. That's because the LOCATE statement requires the text row first (the y coordinate) and then the column (the x coordinate). This is backwards from other coordinate oriented commands:

LOCATE row%, column%

The LOCATE statement can also be used to manipulate the text cursor in other ways. You can use LOCATE to hide or show the flashing cursor and well as control the size and shape of it with optional parameters:

LOCATE row%, column%, cursor%, cursorstart%, cursorstop%

cursor% can be set to 0 to turn the text cursor off or 1 to turn the text cursor on. 

The text cursor is made up of 31 scan lines and cursorstart% and cursorstop% can be used to control which of those scan lines are used to change the shape of the text cursor. Both of these optional parameters can accept a value between 0 and 31.

The CSRLIN statement is used retrieve the current text row the cursor resides in (the y coordinate) and POS is used to retrieve the current text column the cursor resides at (the x coordinate). Here is another demonstration of these two commands being used together.

( This code can be found at .\tutorial\Lesson9\CsrlinPosDemo.bas )

'*
'* CSRLIN and POS demo
'*

DIM Row%, Column% ' row and column of saved cursor position
DIM Count% '        generic counter

RANDOMIZE TIMER '                                        seed random number generator
CLS '                                                    clear screen
PRINT "Let's print a random number of lines." '          inform user what's happening
PRINT
SLEEP 5 '                                                pause for 5 seconds
FOR Count% = 1 TO 5 + INT(RND * 5) + 1 '                 cycle from 5 up to 10
   PRINT "Random line number ->"; Count% '               show user the random lines
   _DELAY .5 '                                           pause for 1/2 second
NEXT Count%
PRINT '                                                  inform user of progress
PRINT "*** Now for a random length of characters. ***"
PRINT
SLEEP 5 '                                                pause for 5 seconds
Rand% = 20 + INT(RND * 40) + 1 '                         generate random number between 20 and 60
PRINT STRING$(Rand%, "*"); '                             print that many asterisks (notice the ; at end)
Row% = CSRLIN '                                          get current row position of text cursor
Column% = POS(0) '                                       get current column position of text cursor
LOCATE Row% + 1, Column% '                               place text cursor directly under that spot
PRINT CHR$(24); '                                        print an up arrow symbol
LOCATE Row% + 2, Column% - 14 '                          inform user what happened
PRINT "This position has been saved"
LOCATE Row% + 3, Column% - 11
PRINT "with CSRLIN and POS(0)."
LOCATE Row% + 5, Column% - 13
PRINT "Lets do a countdown there."
SLEEP 5 '                                                pause for 5 seconds
LOCATE Row%, 1 '                                         position cursor at the beginning of the asterisk line
PRINT SPACE$(70) '                                       remove the asterisks
FOR Count% = 10 TO 1 STEP -1 '                           cycle from 10 to 1
   LOCATE Row%, Column% '                                position the text cursor at the saved location
   PRINT _TRIM$(STR$(Count%)); " "; '                    print current count down value
   _DELAY .5 '                                           pause for 1/2 second
NEXT Count%

Figure 7: Using CSRLIN and POS to save the text cursor location

The above code is fairly straight forward however there is one thing to point out. After the string of asterisks has been printed in line 22 there is a semicolon ( ; ) immediately following the PRINT command. This is to keep the text cursor from moving to the beginning of the next line. Remember that a semicolon used within a PRINT statement tells the text cursor to stay where it is and print what ever follows right after. It's also a handy way to keep the text cursor where you want it as is this case with this code.

The POS statement curiously requires a value within parenthesis after it:

Column% = POS(0) '                                       get current column position of text cursor

However the value of this number means nothing and 0 is usually placed there. It can be any integer value you wish, but again means nothing.

The Semicolon (;), Comma (,) and TAB Statement

The LOCATE, CSRLIN, and POS  statements are great for precise control of the text cursor anywhere on the screen. If all you need is a little text cursor control on a single line however then the semicolon ( ; ), comma ( , ), and TAB statement are what you need.

The semicolon tells the PRINT command to leave the text cursor at the end of the PRINT statement instead of the default behavior of moving the cursor to the next line (also known as a CR/LF or Carriage Return/Line Feed). The following lines of code prints two literal strings on separate lines:

PRINT "Hello there."
PRINT "My name is Bob."

No mystery there as this is the expected behavior. However, add a semicolon ( ; ) after the first PRINT statement and things change:

PRINT "Hello There.";
PRINT "My name is Bob."

Even though two PRINT statements were used they both printed to the same line. The semicolon ( ; ) told the first PRINT statement to leave the text cursor alone and let it remain where it is. The second PRINT statement uses that text cursor position to print its string of information. This can be used to create complex lines of information with a single PRINT statement:

Fname$ = "Bob"
Age% = 28
Job$ = "programmer"
PRINT "Hello there. My name is "; Fname$; ". I'm"; Age%; "years old and I'm a "; Job$; "."

The comma ( , ) is used in the same manner as a semicolon ( ; ) but instead of leaving the text cursor at the end of the line it moves it to the next tab position on the current line line. All screens have hidden tab points on them spaced 15 characters apart. This is a throwback from the function of typewriters that would move the platen over to the next tab position by pressing the TAB key. This was a quick way for a typist to either move the platen quickly to the left or line up fields of information easily. The next lines of code show this behavior:

PRINT
FOR count% = 1 TO 5
   PRINT "Column"; count%, ' neatly spaced columns 15 characters apart
NEXT count%
PRINT: PRINT
FOR count% = 1 TO 5
   PRINT "13 Characters", '  neatly spaced columns 15 characters apart
NEXT count%
PRINT: PRINT
FOR count% = 1 TO 5
   PRINT "14 Characters.", ' too many characters to neatly space in columns using a comma
NEXT count%

The first two FOR ... NEXT loops lined up the output in nice neat columns spaced 15 characters apart. However, you can only have up to 13 characters in a column. If you place more than 13 characters in a column the next column is skipped in favor of the one after that as seen with the last FOR ... NEXT behavior.

If you need precise control of tab positions on a line then the TAB statement is needed. Instead of the default 15 characters you can control where the next tab position occurs.

PRINT
FOR count% = 1 TO 8
   PRINT "Tab"; (count% - 1) * 10; TAB(count% * 10); 'neatly spaced columns 10 characters apart
NEXT count%
PRINT: PRINT
FOR count% = 1 TO 8
   PRINT "13 Characters"; TAB(count% * 10); '         too many characters to neatly space
NEXT count%

The first FOR ... NEXT loop lines the text up in 8 columns tabbed 10 spaces apart as expected. The TAB(count% * 10); at the end of each line within the loop moved the cursor to that calculated tab position.

However, if the next TAB position is not available because it has already been used the TAB statement will use that position on the following line. This can lead to unexpected results as seen in the second FOR ... NEXT loop.

The COLOR Statement

The COLOR statement is used to change the background and foreground color of text to be printed to the screen. The COLOR statement needs to parameters:

COLOR foreground&, background&

The foreground& is the color of the text and the background& is the color strip contained behind the text. Now that you can manipulate strings of data it's time to give those strings a dash of color. Type in the example code.

( This code can be found at .\tutorial\Lesson9\ColorDemo.bas )

'* SCREEN 0 COLOR Demo

DIM fg&, bg& '     foreground and background color
DIM f$, b$, bf$ '  right aligned foreground, background, and blinking values

DO '                                                       begin main loop
   COLOR 7, 0 '                                            white text on black background
   CLS
   PRINT TAB(22); "************************************" ' print header
   PRINT TAB(22); "** SCREEN 0 text color attributes **"
   PRINT TAB(22); "************************************"
   PRINT
   PRINT TAB(34); "Page"; STR$(bg& + 1); " of 8"
   FOR fg& = 0 TO 15 '                                     cycle through 16 foreground colors
        f$ = RIGHT$(" " + _TRIM$(STR$(fg&)), 2) '          create right aligned numbers
        bf$ = RIGHT$(" " + _TRIM$(STR$(fg& + 16)), 2) '
        b$ = RIGHT$(" " + _TRIM$(STR$(bg&)), 2)
       LOCATE CSRLIN, 12 '                                 position text cursor
       COLOR fg&, bg& '                                    set text color
       PRINT "  This is COLOR "; f$; ","; b$, '            print non-blinking colored text
       COLOR fg& + 16, bg& '                               set text color
       PRINT "  This is COLOR "; bf$; ","; b$; "  " '      print blinking colored text
   NEXT fg&
   COLOR 7, 0 '                                            white text on black background

   PRINT
   PRINT TAB(27); "Press ENTER for next color" '           print instructions
   PRINT TAB(34); "ESC to exit";
   WHILE INKEY$ = "": _LIMIT 10: WEND '                    wait for a key press
    bg& = bg& + 1 '                                        increment to next background color
LOOP UNTIL bg& = 8 OR _KEYDOWN(27) '                       leave loop when finished or ESC pressed
SYSTEM '                                                   return to operating system

When the above code is executed the column of the text on the right is blinking. Foreground color values can be in the ranges of:

Background color values can range from 0 to 7 for a total of eight. The standard SCREEN 0 text and background colors are:

Note: The color blue indicated for value 1 above has been altered. The true color of blue for position 1 was too difficult to see given the blue background of this page. The same goes for black.

Legacy SCREENs for the most part have these color limitations however 32bit screens do not. Type in the following code.

( This code can be found at .\tutorial\Lesson9\32bitColorDemo.bas )

'* 32bit COLOR Demo

TYPE RGB '            fading RGB definition
    r AS INTEGER '    red component
    g AS INTEGER '    green component
    b AS INTEGER '    blue component
    rdir AS INTEGER ' red fade direction
    gdir AS INTEGER ' green fade direction
    bdir AS INTEGER ' blue fade direction
END TYPE

DIM RGB(203) AS RGB ' create fade array
DIM c% '              generic counter

RANDOMIZE TIMER '                                               seed random number generator
FOR c% = 1 TO 203 '                                             cycle 203 times
    RGB(c%).r = INT(RND * 240) + 5 '                            random red, green, blue components
    RGB(c%).g = INT(RND * 240) + 5 '                            between 5 and 250
    RGB(c%).b = INT(RND * 240) + 5
   DO
        RGB(c%).rdir = INT(RND * 2) - INT(RND * 2) '            random red, green, blue directions
        RGB(c%).gdir = INT(RND * 2) - INT(RND * 2) '            between -1 and 1
        RGB(c%).bdir = INT(RND * 2) - INT(RND * 2)
   LOOP UNTIL RGB(c%).rdir AND RGB(c%).gdir AND RGB(c%).bdir '  make sure no direction is zero
NEXT c%
SCREEN _NEWIMAGE(640, 480, 32) '                                32bit graphics screen
DO '                                                            begin main loop
   _LIMIT 120 '                                                 120 frames per second
   FOR c% = 1 TO 203 '                                          cycle 203 times
       COLOR _RGB32(RGB(c%).r, RGB(c%).g, RGB(c%).b), 0 '       set next text color
       PRINT "Psychodelic"; '                                   print text in that color
       IF RGB(c%).r = 255 OR RGB(c%).r = 1 THEN '               has red component reached limit?
            RGB(c%).rdir = -RGB(c%).rdir '                      yes, reverse direction
       END IF
        RGB(c%).r = RGB(c%).r + RGB(c%).rdir '                  increment red component
       IF RGB(c%).g = 255 OR RGB(c%).g = 1 THEN '               has green component reached limit?
            RGB(c%).gdir = -RGB(c%).gdir '                      yes, reverse direction
       END IF
        RGB(c%).g = RGB(c%).g + RGB(c%).gdir '                  increment green component
       IF RGB(c%).b = 255 OR RGB(c%).b = 1 THEN '               has blue component reached limit?
            RGB(c%).bdir = -RGB(c%).bdir '                      yes, reverse direction
       END IF
        RGB(c%).b = RGB(c%).b + RGB(c%).bdir '                  increment blue component
   NEXT c%
   _DISPLAY '                                                   display updates made to screen
LOOP UNTIL _KEYDOWN(27) '                                       leave when ESC key pressed
SYSTEM '                                                        return to operating system

Figure 8: Groovy

As Figure 8 shows you can assign any _RGB32 value to COLOR to get the full spectrum of colors in a 32bit screen as shown in line 30 of the code above.

The PRINT USING Statement

The PRINT USING statement is used to print text in a predetermined format by supplying a template to use. This is a very powerful statement and one that too often gets overlooked. To understand how it works type in the example code below.

( This code can be found at .\tutorial\Lesson9\UsingDemo.bas )

'* PRINT USING Demo

DIM fn$ '  first name
DIM ln$ '  last name
DIM afn$ ' aka first name
DIM aln$ ' aka last name
DIM ch$ '  character name
DIM dj% '  date joined
DIM cb% '  cyan bar toggle flag

SCREEN _NEWIMAGE(585, 630, 32) '                                  set up screen
CLS , _RGB32(255, 255, 255) '                                     white background
COLOR _RGB32(0, 0, 0), _RGB32(255, 255, 255) '                    black text on white background
PRINT
PRINT
"                  Avengers Roster 1960's through 1980's" ' print header
PRINT
PRINT
"     FIRST       LAST              AKA               CHARACTER     DATE"
PRINT "     NAME        NAME        FIRST       LAST          NAME        JOIN"
PRINT "  ----------  ----------  ----------  ----------  ---------------  ----"
Format$ = "  \        \  \        \  \        \  \        \  \             \  ####  " ' output formatter
cb% = 1
DO '                                                             begin main loop
   READ fn$, ln$, afn$, aln$, ch$, dj% '                         read in data fields
   COLOR _RGB32(0, 0, 0), _RGB32(255, 255, 255) '                black text on white background
   IF dj% THEN '                                                 end of data?
       IF cb% THEN COLOR _RGB32(0, 0, 0), _RGB32(0, 223, 223) '  no, cyan text background every other line
       PRINT USING Format$; fn$; ln$; afn$; aln$; ch$; dj% '     print formatted data fields
        cb% = 1 - cb% '                                          toggle cyan background flag
   END IF
LOOP UNTIL dj% = 0 '                                             leave when no more data
SLEEP '                                                          wait for key press
SYSTEM '                                                         return to operating system

' DATA statements used to simulate getting data from source such as a file

DATA "Henry","Jonathan","Hank","Pym","Ant Man",1963,"Janet","VanDyne","Janet","Pym","Wasp",1963
DATA "Anthony","Edward","Tony","Stark","Iron Man",1963,"Robert","Banner","Bruce","Banner","The Hulk",1963
DATA "Thor","Odinson"," "," ","Thor",1963,"Richard","Milhouse","Rick","Jones","Honorary Member",1963
DATA "Steven","Rogers"," "," ","Captain America",1964,"Clinton","Barton"," "," ","Hawkeye",1965
DATA "Pietro","Maximoff","Pietro","Frank","Quicksilver",1965
DATA "Wanda","Maximoff","Wanda","Frank","Scarlet Witch",1965,"Heracles"," ","Harry","Cleese","Hercules",1967
DATA "T'Challa"," "," "," ","Black Panther",1968,"Victor","Shade"," "," ","Vision",1968
DATA "Dane","Whitman"," "," ","Black Knight",1969,"Natalia","Romanova","Natasha","Romanoff","Black Widow",1973
DATA "Brandt"," "," "," ","Mantis",1973,"Henry","McCoy"," "," ","Beast",1975
DATA "Heather","Douglas"," "," ","Moondragon",1975,"Patsy","Walker","Patsy","Hellstrom","Hellcat",1975
DATA "Matthew","Liebowitz","Matthew","Hawk","Two-Gun Kid",1975,"Robert","Frank"," "," ","Whizzer",1977
DATA "Simon","Williams"," "," ","Wonder Man",1977,"Yondu","Udonta"," "," ","Yondu",1978
DATA "Mar-Vell"," "," "," ","Captain Marvel",1978,"Samuel","Wilson"," "," ","Falcon",1979
DATA "Jennifer","Walters"," "," ","She-Hulk",1982,"Monica","Rambeau"," "," ","Spectrum",1983
DATA "Eros"," "," "," ","Starfox",1983,"James","Rhodes","Iron","Patriot","War Machine",1984
DATA "Barbara","Barton","Agent 19"," ","Mockingbird",1984,"Benjamin","Grimm"," "," ","Thing",1986
DATA " "," "," "," "," ",0000

Figure 9: Super hero roster

Where are all of the string manipulation commands to create the chart seen in Figure 9 above? There are not even any uses of semicolons, commas, or the TAB statement to get this output. That's the power of the PRINT USING command.

The reason this command is so often overlooked is because it was created to aid in formatting text on wide carriage printers that contained 132 columns and on texted based monochrome terminals back in the very early days of computing. Most of the old-school languages, such as COBOL, had statements like this too. When those went out of style programmers simply left this command behind. But as you can see this is an extremely useful tool at your disposal.

The secret lies in line 20 of the code. A string variable named Format$ contains a pre-made template on how to format data given to it. The "\      \" fields within the template means to keep a string value within this area. The "####" field at the end means to keep a numeric value within this area.

Format$ = "  \        \  \        \  \        \  \        \  \             \  ####  " ' output formatter

Format$ has been set up to receive 5 strings and a numeric value. When the first string is passed in its placed in the first "\     \" field, the second string in the second "\     \" field, and so on. The last field expects a numeric value to format within its "####" area. This is done in line 27 of the code.

PRINT USING Format$; fn$; ln$; afn$; aln$; ch$; dj% '     print formatted data fields

The five strings passed in are placed in the "\     \" fields and the integer is placed in the "####" field automatically. This would be mind-numbing If you had to do this with string manipulation commands such as LEFT$, RIGHT$, STR$, etc.. and then concatenate the formatted strings together. Using semicolons, commas, and TABs would be a pain too because you would always need to account for the length of the individual values so as not to mess the column alignment up.

There is a table of all the available formatting characters to be used with PRINT USING in the QB64 Wiki. The best way to get familiar with the PRINT USING statement is to play around with the various formatting characters. For instance, adding a dollar sign to a value to make it a monetary value and then printing it could be done like this:

Payout! = 100 '                     pay day!
po$ = "$" + _TRIM$(STR$(Payout!)) ' insert $ at beginning
decimal% = INSTR(po$, ".") '        get position of decimal point
IF decimal% = 0 THEN '              was there a decimal point?
    po$ = po$ + ".00" '             no, add .00 to end
ELSE '                              yes, a decimal point exists
    po$ = po$ + "00" '              append 00 to end to assure at least two places
    po$ = LEFT$(po$, decimal%) + MID$(po$, decimal% + 1, 2) ' build monetary string
END IF
PRINT po$ '                         phew! that was a lot of work

The above code ensures that no matter what the value in Payout! is, 100, 100.1, 100.99, etc.. that a decimal point is taken into account and the number of places in the decimal area is always 2. Or you could simply do this:

PRINT USING "$###.##"; Payout! '    easy peasy!

No string manipulation needed and no worrying about the decimal point. Again, very powerful!

A String Manipulation Demo

Here is a sample program that uses string manipulation to create a hidden password function.

( This code can be found at .\tutorial\Lesson9\HiddenPassword.bas )

'--------------------------------          ********************************************************
'- Variable Declaration Section -          * Simple hidden password demo highlighting some of the *
'--------------------------------          * string manipulation commands available in QB64.      *
'                                          *                                                      *
DIM Login$ '    login name user supplies   * LEN() - returns the length of a string               *
DIM Password$ ' password user supplies     * ASC() - returns the ASCII value of string character  *
'                                          * LEFT$() - returns left # of characters of a string   *
'----------------------------              * STRING$() - returns a string of same characters      *
'- Main Program Begins Here -              ********************************************************
'----------------------------

PRINT
PRINT
" ------------------------"
PRINT " - Ritchie's Web Server -"
PRINT " ------------------------"
PRINT
PRINT
" Welcome to my web server!"
PRINT
PRINT
" Before you can begin, you must create an account."
DO '                                                                      begin login loop
   PRINT '                                                                blank line
   PRINT " Create a login name between 6 and 16 characters in length ." ' prompt user
   LINE INPUT " Login    > ", Login$ '                                    get login name
LOOP UNTIL LEN(Login$) > 5 AND LEN(Login$) < 17 '                         continue if length ok
DO '                                                                      begin password loop
    Password$ = "" '                                                      clear current password
   DO '                                                                   begin pwd length loop
       PRINT " Enter a password that is at least 8 characters long." '    prompt user
        Password$ = GetPassword$ '                                        get password from user
   LOOP UNTIL LEN(Password$) > 7 '                                        continue if length ok
   PRINT " Please verify the password by typing it in again." '           prompt user
LOOP UNTIL Password$ = GetPassword$ '                                     continue if same pwd
PRINT '                                                                   blank line
PRINT " Remember for your records:" '                                     inform user
PRINT '                                                                   blank line
PRINT " Login    > "; Login$ '                                            display login name
PRINT " Password > "; Password$ '                                         display password
END '                                                                     end program

'-----------------------------------
'- Function and Subroutine section -
'-----------------------------------

'--------------------------------------------------------------------------------------------------

FUNCTION GetPassword$ ()

    '**************************************************************************************************
    '* Prompts the user for a password. As user types in password the keystrokes are displayed as     *
    '* asterisks. The back space key is recognized. When the user presses the ENTER key the password  *
    '* entered by the user is sent back to the calling routine.                                       *
    '**************************************************************************************************

    '---------------------------
    '- Declare local variables -
    '---------------------------

   DIM Cursorline% ' current Y location of cursor
   DIM CursorPos% '  current X location of cursor
   DIM Password$ '   password created by user
   DIM KeyPress$ '   records key presses of user

    '------------------------
    '- Function begins here -
    '------------------------

   PRINT " Password > "; '                                     prompt user for input
    Cursorline% = CSRLIN '                                     save cursor Y position
    CursorPos% = POS(0) '                                      save cursor X location
   DO '                                                        begin main loop
       DO '                                                    begin key press loop
           _LIMIT 30 '                                         limit to 30 loops per sec
            KeyPress$ = INKEY$ '                               get key user presses
       LOOP UNTIL KeyPress$ <> "" '                            loop back if no key pressed
       IF ASC(KeyPress$) > 31 THEN '                           was key pressed printable?
            Password$ = Password$ + KeyPress$ '                yes, add it to password string
       ELSEIF ASC(KeyPress$) = 8 THEN '                        no, was it the back space key?
            Password$ = LEFT$(Password$, LEN(Password$) - 1) ' yes, remove rightmost character
       END IF
       LOCATE Cursorline%, CursorPos% '                        position cursor on screen
       PRINT STRING$(LEN(Password$), "*"); " "; '              print string of asterisks
   LOOP UNTIL KeyPress$ = CHR$(13) '                           end main loop if ENTER pressed
   PRINT '                                                     move cursor from end of asterisks
    GetPassword$ = Password$ '                                 return the password user supplied

END FUNCTION

'--------------------------------------------------------------------------------------------------

Figure 10: Don't peek!

Your Turn

Create a program that scans a user supplied sentence and counts the individual letters found within the sentence. Figure 11 below shows how the program should execute.

Figure 11: Counting letters

- Each letter in the sentence should be counted regardless of being upper or lower case.
- No graphics screen is needed. The program should run in a standard text screen.
- Save the program as LetterCount.BAS when finished.

Click here to see the solution.

Commands and Concepts Learned