Building a Better BASIC Input

It looks like I am going to be writing an update to my 1983 *ALLRAM* BBS. It was a simple bulletin board system that could be operated from cassette (no disk drives required!).

I have decided to use this as an excuse to create a few more articles that will allow me to experiment with better ways to do things a BBS might need to do.

Today … Custom input.

Custom Input

BASIC’s INPUT and LINE INPUT commands are very useful, but no matter how much data you really want, on the CoCo’s Color BASIC these commands allow the user to enter up to 249 characters. Because of this, you always have to truncate the string to what you really want it to be after input:

10 LINE INPUT "NAME :";NM$:NM$=LEFT$(NM$,20)

Still, since BASIC is in control, the user could easily type well more than 20 characters and clutter up the display. Allowing the user to type super-huge strings can also be the cause of a program crash due to Out of String Space errors (more on this in an upcoming article about Strings).

If you want more control over how much, and even what. is allowed to be typed, you can write your own custom input routine.

In a previous article, we looked at using INKEY$ to respond to single-key presses. A brute-force version might look like this:

10 PRINT "YES OR NO? ";
20 A$=INKEY$:IF A$="" THEN 20
30 IF A$="Y" THEN 100
40 IF A$="N" THEN 200
50 PRINT "YES OR NO ONLY, PLEASE!"
60 GOTO 10
100 PRINT "YES!"
110 END
200 PRINT "NO!"
210 END

You could turn that in to a subroutine and call it any time you wanted a YES or NO answer.

One problem with that is it only checks for UPPERCASE “Y” and “N”, so it won’t recognize lowercase. We’d need to add a bit more…

30 IF A$="Y" OR A$="y" THEN 100
40 IF A$="N" OR A$="n" THEN 200

If you had a dozen choices, that would get messy real quick.

To easily handle more choices (and make it faster), we also looked at using INSTR to turn the keyboard choice in to a number, and dispatching it with ON GOTO/GOSUB. Here is a routine that would handle UPPERCASE and lowercase:

10 PRINT "YES OR NO? ";
20 A$=INKEY$:IF A$="" THEN 20
30 LN=INSTR("YyNn",A$):IF LN=0 THEN PRINT"YES OR NO ONLY!":GOTO 20
40 ON LN GOTO 100,100,200,200
50 GOTO 20
100 PRINT "YES!"
110 END
200 PRINT "NO!"
210 END

See what we did there? Now we scan for four characters, “YyNn”, and if it’s one of the first two, we go to the Yes routine on line 100, or for the last two, we go to the No routine on line 200.

We could have also checked the key pressed to see if it was in lowercase and, if it was, convert it to uppercase. Then we only have to compare for uppercase:

10 PRINT "YES OR NO?"
20 A$=INKEY$:IF A$="" THEN 20
25 CH=ASC(A$):IF CH>96 AND CH<123 THEN A$=CHR$(CH-32)
30 LN=INSTR("YN",A$):IF LN=0 THEN PRINT"YES OR NO ONLY!":GOTO 20
40 ON LN GOTO 100,200
50 GOTO 20
100 PRINT "YES!"
110 END
200 PRINT "NO!"
210 END

Line 25 converts the key the user pressed in to the ASCII value. If that value is greater than 96 (the character right before lowercase “a”) and lower than character 123 (the character right after lowercase “z”) it makes A$ the character 32 lower from it, which shifts it from where the lowercase ASCII characters are (97-122) down to the uppercase ASCII characters (65-90).

Benchmark Digression

Note that I chose to check CH>96 (the character BEFORE “a”) instead of CH>=97 (it’s easier to understand what that is, since 97 is “a”). I figured doing “>” instead of “>=” would be a bit quicker since there is less to parse.

I could have also compared the string directly:

40 IF A$>="a" AND A$<="z" THEN A$=CHR$(ASC(A$)-32)

…but I figured that would be slower.

I don’t need to “figure.” Let’s do a few quick tests using the benchmark program:

0 DIM A$,C:A$="*"
5 DIM TE,TM,B,A,TT
10 FORA=0TO4:TIMER=0:TM=TIMER
20 FORB=0TO1000
40 C=ASC(A$):IF C>=97 AND C<=122 THEN A$=CHR$(C-32)
70 NEXT:TE=TIMER-TM
80 TT=TT+TE:PRINTA,TE
90 NEXT:PRINTTT/A:END

Converting the string to a number, then comparing using >= and <= produces 960. Wow. That’s slow.

By adjusting the range being compared so we can remove the “=”:

40 C=ASC(A$):IF C>96 AND C<123 THEN A$=CHR$(C-32)

..produces 950. Okay, so the overhead of “>=” versus “>” is not very significant (parsing one extra byte). But, if every little bit helps…

We also learned that HEX numbers were faster, so one quick speed improvement would be to try them like this:

40 C=ASC(A$):IF C>&H60 AND C<&H7B THEN A$=CHR$(C-32)

This drops the 950 to 721. At the very least, we should be using HEX.

What about using string comparisons instead?

40 IF A$>="a" AND A$<="z" THEN A$=CHR$(ASC(A$)-32)

Comparing a one character string variable to a constant string character produces 537! I think we have a winner. It’s almost twice as fast as the original version in this benchmark. And, dropping the “=” would make it a tad faster. But what character is that?

From the Wikipedia ASCII table, before “a” is some backwards apostrophe thing, “`”… But I don’t know how to type that on the CoCo keyboard in BASIC. After the “z” is the left curly brace, “{” and I’m not sure how to type that in BASIC either.

I *could* put them both in strings, like:

X$=CHR$(96):Y$=CHR$(123)

…then later, compare against those:

IF A$>X$ AND A$<Y$ THEN ...

…but the tiny speed saving might not be worth the loss of memory for two extra variables. I guess my point is, this could still be made a tad faster, but it seems using strings is faster, so let me revise my original example:

10 PRINT "YES OR NO?"
20 A$=INKEY$:IF A$="" THEN 20
25 IF A$>="a" AND A$<="z" THEN A$=CHR$(ASC(A$)-32)
30 LN=INSTR("YN",A$):IF LN=0 THEN PRINT"YES OR NO ONLY!":GOTO 20
40 ON LN GOTO 100,200
50 GOTO 20
100 PRINT "YES!"
110 END
200 PRINT "NO!"
210 END

But I digress…

End of Benchmark Digression

Now we only check for UPPERCASE letters. Since we compare against the “a-z” range, numbers and other characters are untouched. I could have just as easily made an input routine that only allowed numbers to be pressed, or a simple “Press any key to continue” routine.

These are just some simple examples of writing  a custom input routine.

But why bother?

No, not why bother with the speed optimizations above (those will make sense later). Why bother making a custom input routing at all?

Many years ago, I needed my own version of LINE INPUT that would prevent the user from typing too much. In BASIC, if you just want the first 10 characters that the user types, as I showed earlier, you can chop off just those ten characters using the LEFT$() function:

10 LINE INPUT "PASSWORD (10 CHARS MAX):";PW$
20 PW$=LEFT$(PW$,10)
30 PRINT "YOU ENTERED: ";PW$
Using LEFT$ to limit how much input you get.

This works fine, but as you can see, the user could still type more text, going down to the next line of the screen. If this input were at the bottom of the screen, the user could type so much that it would start causing the screen to scroll up. This could look messy and ruin your carefully laid out text user interface.

In my case, I was writing input routines for software we (Sub-Etha Software) would be selling and we didn’t want out carefully laid out text user interface to be ruined and look messy.

So, I put together a very simple input routine that used INKEY$ to read each character and add it to a string until the size limit was reached. When you do it yourself, you are responsible for printing the typed characters to the screen and handling things like BACKSPACE and ENTER.

A VERY simple custom input routine might look like this:

10 CLS
20 PRINT@196,"USER ID : [........]"
30 PRINT@228,"PASSWORD: [............]"
40 PRINT@196+11,;:IM=8:GOSUB 1000:ID$=IN$
50 PRINT@228+11,;:IM=12:GOSUB 1000:PW$=IN$
60 PRINT@294,"VERIFYING ACCOUNT..."
70 REM SEARCH FOR ID$ AND PW$
999 END
1000 REM IN : IM=INPUT MAX
1001 REM RET: IN$=STR, IN=SIZE
1002 REM USE: CH$
1010 IN$="":IN=0
1020 CH$=INKEY$:IF CH$="" THEN 1020
1030 CH=ASC(CH$)
1040 IF CH=8 THEN IF IN=0 THEN 1090 ELSE IN=IN-1:IN$=LEFT$(IN$,IN):PRINT CH$;:GOTO 1020
1050 IF CH=13 THEN RETURN
1060 IF CH>=32 AND CH<=127 AND IN<IM THEN IN$=IN$+CH$:IN=IN+1:PRINT CH$;:GOTO 1020
1090 SOUND 200,1:GOTO 1020
Behold! Custom INPUT!

Now we can limit what the user can type (visible characters only) and how much of them they type.

There are some problems with this… There is no cursor, so the user won’t see where they are typing. We could fix that by printing a block character before waiting for a keypress, and erasing it before updating the display:

1000 REM IN : IM=INPUT MAX
1001 REM RET: IN$=STR, IN=SIZE
1002 REM USE: CH$
1010 IN$="":IN=0
1015 IF IN<IM THEN PRINT CHR$(128);
1020 CH$=INKEY$:IF CH$="" THEN 1020
1025 IF IN<IM THEN PRINT CHR$(8);
1030 CH=ASC(CH$)
1040 IF CH=8 THEN IF IN=0 THEN 1090 ELSE IN=IN-1:IN$=LEFT$(IN$,IN):PRINT CH$;:GOTO 1015
1050 IF CH=13 THEN RETURN
1060 IF CH>=32 AND CH<=127 AND IN<IM THEN IN$=IN$+CH$:IN=IN+1:PRINT CH$;:GOTO 1015
1090 SOUND 200,1:GOTO 1015

Now we get a black block for the cursor, but it could be any printable character. I had to add some special checks so the cursor did not print if we were at the max length. That was causing it to print the cursor over the “]” on the screen. Since I control the display and input, I customized this routine specifically for that.

We could also add special modes to this routine to force all the input to be uppercase, or only allow numbers to be typed. But, the more code we add, the slower this routine gets. Fast typists may find this annoying. This is where the speed optimizations can come in handy. Some optimization ideas:

  • Remove all spaces and compact lines together.
  • The comparison numbers can be turned to HEX values, which is faster.
  • The variables used inside the loop can be declared first/earlier, so they are found faster.
  • By relocating this subroutine to the top of the program, all the GOTOs will be found faster.

I was only aware of the first point back in the 1980s when I wrote my original input routines. It looks like this time I have more optimizations to try.

And now, with the BASICs (pun intended) of an INPUT routine discussed, my next goal is to create a fast, and fancy, input routine I can use for the BBS project. I am still deciding how much I want to put in it, such as doing word wrap at the end of typing in messages and such, but I will share my progress here.

Until then…

10 thoughts on “Building a Better BASIC Input

  1. Johann Klasek

    > 30 LN=INSTR(“YyNn”,A$):IF LN=0 THEN PRINT”YES OR NO ONLY!”:GOTO 20
    > 40 ON LN GOTO 100,100,200,200

    Here I would suggest to modify line 40 to look like
    40 ON (LN+1)/2 GOTO 100,200
    and you don’t have to keep the line numbers twice in ON…GOTO. To go with that would be faster too (compared to the uppercase conversion with IF…THEN stuff).

    Or make a faster lower-to-upper function and vice versa like this:
    1 AL$=” aAbB….zZ”
    1000 A=INSTR(AL$,C$): IF A>1 THEN C=64+A/2 ELSE C=ASC(C$)
    assuming C$ is always non-empty.

    In case of a conversion with A$=CHR$(ASC(A$)-32) this could be a slightly faster: A$=CHR$(ASC(A$)AND M) but only with a predefinition of M=223 (with constant 223 it would be slower!) which masks bit 5 (value 32) in the character code representation.

    Reply
    1. Allen Huffman Post author

      Very clever case change using the string. I will benchmark those. I would have thought doing (LN+1)/2 would have been slower due to the parsing and math but I have been wrong about many things that I thought would be slower :)

      Thanks!

      Reply
  2. Johann Klasek

    Regarding optimization ideas:
    > By relocating this subroutine to the top of the program, all the GOTOs will be found faster.
    Why not prevent back-GOTOs with FOR-NEXT constructions …
    1015 FOR I=.TO1STEP.: IF …

    and going back with simply a NEXT instead of GOTO1015. If in a subroutine, the open FOR-NEXT does not harm because RETURN frees the FOR-NEXT stack frame too.

    For the inner loop: instead of
    1020 CH$=INKEY$:IF CH$=”” THEN 1020
    something like that
    1020 FOR J=-1TO.:CH$=INKEY$:J=CH$=””:NEXT
    looks strange, but J= is the assignment, CH$=”” the comparison as logical expression giving 0 (false) or -1 (true), which may look prettier as J=(CH$=””)
    For all this you have to keep the used FOR-variables in mind to prevent any interference with the calling level.

    Reply
      1. Johann Klasek

        First, “J=” the abbreviation for LET J=… and is the assignment. Everything following this is just an expression to be evaluated. A this stage “=” is just a 2-value operator, like other comparison operators “” and combinations of them (like “”). In combination with logical operators like AND, OR, NOT these can be used to built complex expressions. All these operators are processed according to their priority (if not ordered explicitly by parentheses). To focus on comparisons: These are just functions taking 2 parameters resulting in true (equivalent to -1) or false (equivalent to 0), which corresponds to all bits set or all bits cleared, respectively, which in turn can be easily used to calculate logical expressions with OR, AND or NOT as bitwise operators.
        Such a expression may appear not only between IF and THEN, but everywhere an expression is evaluated, especially on the right side of an assignment – closing the circle.

        Reply
      2. William Astle

        Johann’s explanation is accurate. I just figured I’d chime in with some details that might make things even less clear.

        The comparison (“relational”) operators return -1 for true and 0 for false. You don’t need to worry about bit patterns there for most usaes. AND, OR, and NOT operate on 16 bit integers and perform bitwise operations. NOT is the “one’s complement”. (The ordinary negation operator would be the equivalent of the “two’s complement”.)

        You can exploit this nicely in things like Johann’s example above to avoid the need for line numbers and GOTO. I expect you’ll find there are a lot of tricks like that used in those old one and two liners from Rainbow.

        You can also potentially exploit it for DEFFN functions that need to do different things depending on the input value.

        Just to blow your mind, try working out what this does:

        IF0THENIF0THENPRINT1ELSEPRINT2ELSEPRINT3

        Reply
        1. Allen Huffman Post author

          BASIC09 forever caused me to see things differently:

          IF 0 THEN
              IF 0 THEN
                  PRINT 1
              ELSE
                  PRINT 2
          ELSE
              PRINT 3
          

          …and in C, 0 would be false, so I am thinking it would print 3.

          Reply
          1. William Astle

            Yeah, that’s what it does. Most people don’t realize you can do that, though. You do have to have a full IF/ELSE setup for any nested IF statements, though, since ELSE binds tightly. If you’ve ever seen bits that read “ELSEELSE” or whatever, that’s probably why. (You’d need ENDIF to avoid having empty ELSE clauses.) You can get some seriously crazy stuff going with nested IF statements, FOR/NEXT tricks, and using relational operators in normal expressions. We’re talking IOCCC level obfuscation.

  3. Pingback: Building a Better BASIC Input, revisited | Sub-Etha Software

  4. Pingback: Make BASIC Fast Again, part 4 – Vintage is the New Old, Retro Games News, Retro Gaming, Retro Computing

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.