the math wizard
on the web
Formatting Text into Checkese

Contact and Disclaimer

You may reach the Math Wizard for questions or comments about this topic at this email address: [ math_wizard44@hotmail.com ] Feel free to share with my thoughts, as long as these messages do not constitute destructive criticism.

This code has been tested to work with near perfect accuracy in home computers running the QBasic 1.1 Interpreter from Microsoft. It has not been tested as part of any subsystem that handles mission-critical (or otherwise critical) data, nor has it been designed for use of that purpose in mind. The main purpose of this code is to demonstrate how QBasic can be used in a problem such as Checkese (see below).

When using/changing this public domain code, keep in mind that I make no representation about its ability and/or fitness, and that you should test all public-domain code thoroughly before using it in your own programs.

Introduction

I myself, as a little boy, have always wondered why checks had to have the English wording of the Amount of Currency included. I've always thought it silly if someone who already knew how to read numbers would need this extra system. Perhaps you are still wondering.

The reason is that people, because of their handwriting, may commit mistakes rendering a number amount unreadable; this opens the value to interpretation. A smudged "3" might look like an "8" to a banker with an eye problem. A writer of a check might misplace the decimal point, making the amount ten times as large or as small as intended.

For this reason, wording currency values in English (from now on referred to as "Checkese") is a sort of check to see if the numeral value is indeed correct. This low level of technology might save avid check writers some money.

Writing the Code

We are going to write a program (or more properly, a "function") that returns the input currency value in Checkese. So we write this into the environment as such:

 
DECLARE FUNCTION Checkese$ (Amount AS STRING)

This does nothing but declare the function we are about to make.

You will note some things about this function. One in particular is that it is designed to return a String value (as indicated by the $ in the function name). This is important because Checkese, which is a "word-for-word" representation of a value, will use letters and ASCII characters.

Another is that the parameter "Amount" is also a string. This makes it easier to read in and manipulate the individual digits of the data, which we will be doing a lot of in this function. A longer explanation is provided further down.

Now we make a new function named Checkese and whose only parameter is a string named Amount. We then make the value in Amount a local variable to that function.

 
FUNCTION Checkese$ (Amount AS STRING)

DIM Amt         AS STRING
DIM WholeValue  AS STRING
DIM DecimValue  AS STRING

DIM Chunk       AS STRING
DIM ChunkVal    AS STRING
DIM SVal        AS STRING

DIM Value       AS INTEGER
DIM OnesValue   AS INTEGER
DIM NumGroups   AS INTEGER

Amt = Amount

We have now converted the value in Amount into a string which we can now read and manipulate sequentially. This is important as we will have the program "read", or make sense out of, the input, and convert these digits into Checkese. We also made variables "WholeValue" and "DecimValue" that will store parts of the finished string. The other variables are holders for temporary data.

Now you might be wondering why our function only takes strings of integers. This is due to a convention to the way we are going to write down our currency values. In this convention, we are going to get rid of the need to use decimals in our value. For example, if we wanted the function to accept a value such as [ $ 5,236.23 ], we will write it thus:

$ 5,326.23      >       523623

The last two digits of the input value will always be after the decimal point (as is in most current systems). If we wanted ten thousand dollars [ $ 10,000.00 ], we will write it thus:

$ 10,000.00     >       1000000

We need to add two extra zeroes to stand for the cents value if we want a whole number. (Notice as well that we do not need the employment of commas; commas are a means to aid in the reading of values.)

I will explain below how the function will read the given string.

    Read the last two digits of the StartString (the decimal portion).

    Place its Checkese equivalent to the DecimValue.

    Delete these last two digits from the StartString.

    While there are more than zero characters in the StartString: 

      Read the last three digits of the StartString. (If there are less, append zeroes.)

      Append its Checkese equivalent to the beginning of the WholeValue.

      Delete these last three or less digits from the Start String.

    Combine the strings from Step 2 (DecimValue) and Step 4 (WholeValue) into one.

    Place this value from Step 5 as the return value of the function.

Notice that this procedure reads from the right of the value to its left. This is because it is predictable what the next translated value (or "chunk", if you will) would be when you start from the right. Consider the following example, wherein the computer reads the value sequentially from the left. 
678750

It would not know (without being told beforehand by some other means) that the first 6 is grouped by itself: [ six thousand ]. However, it knows (because from the logic of our numbering convention) that the last two digits are to be coded as [ 50/100 dollars ]. Once these are deleted, we can take the next three digits [787] and code them as [ seven hundred and eighty-seven ]. And once these are deleted, the computer can now clearly see that [6] is coded as [ Six thousand ].

Of course, for the computer to be able to translate a value to Checkese (which is in essence English), we must teach it a few English words. So we will build three arrays to store some words the computer needs to know to translate with.

    The first would be GroupingAppend(), an array that contains words such as "Million", "Thousand", and "Billion", words that set off groups of three digits (hence their name).

    The second would be a two-dimensional one called Numeral(). It would translate digit values (like "two" as opposed to "twenty"). Take note of the entry at Numeral(2, 1). It is the syllable "TEEN", and thus the number "11" would (at first) be translated as [ TEEN-one ].

    The third array would contain the names of numbers from [10] through [19], the "TEEN" numbers. A special part of the translation subroutine would replace a string like [ TEEN-one ] to [ eleven ] according to this array. This array would be called Teen().

Here is the code for these three arrays.

 
DIM GroupingAppend(1 TO 6) AS STRING
        GroupingAppend(1) = ""
        GroupingAppend(2) = "thousand"
        GroupingAppend(3) = "million"
        GroupingAppend(4) = "billion"
        GroupingAppend(5) = "trillion"
        GroupingAppend(6) = "quadrillion"

       
DIM Numeral(1 TO 2, 0 TO 9) AS STRING
        Numeral(1, 0) = "": Numeral(2, 0) = "and "
        Numeral(1, 1) = "one": Numeral(2, 1) = "TEEN-"
        Numeral(1, 2) = "two": Numeral(2, 2) = "twenty"
        Numeral(1, 3) = "three": Numeral(2, 3) = "thirty"
        Numeral(1, 4) = "four": Numeral(2, 4) = "forty"
        Numeral(1, 5) = "five": Numeral(2, 5) = "fifty"
        Numeral(1, 6) = "six": Numeral(2, 6) = "sixty"
        Numeral(1, 7) = "seven": Numeral(2, 7) = "seventy"

        Numeral(1, 8) = "eight": Numeral(2, 8) = "eighty"
        Numeral(1, 9) = "nine": Numeral(2, 9) = "ninety"

DIM Teen(0 TO 9) AS STRING
        Teen(0) = "ten"
        Teen(1) = "eleven"
        Teen(2) = "twelve"
        Teen(3) = "thirteen"
        Teen(4) = "fourteen"
        Teen(5) = "fifteen"
        Teen(6) = "sixteen"

        Teen(7) = "seventeen"
        Teen(8) = "eighteen"
        Teen(9) = "nineteen"

We now have to construct the "translator". There will be two: one that translates the last two digits, and another to handle the groups of three digits we have allowed for. We will first make the translator for the last two digits, since it is the most intuitive.

We need a function (RIGHT$() in QBasic) that will take the last digits from the Start String. Here is the code to do Steps 1 to 3.

 
' // Get Decimals //

Chunk = RIGHT$(Amt, 2)
DecimValue = Chunk + "/100 dollars"
Amt = LEFT$(Amt, LEN(Amt) - 2)

It is as simple as it looks, while the last statement might need some explanation. The equivalent of "deleting" the last two characters from the string is to keep the first [(Length of Amt) - 2] characters of Amt, and to discard the rest (the last two).

What is left now is the most difficult part of the procedure. We need to carry out Step 4 as a conditional loop. The loop and part of its setup will look something like this.

 
' // Get All Other Digits //

NumGroups = 1
DO UNTIL LEN(Amt) < 1
.
.
.
LOOP

Notice that NumGroups is set to 1. This value will tell the procedure what grouping symbol (thousand, million, and so on) to append to the result.

Here is the code for Step 4 of our algorithm.

 
' // Get All Other Digits //

NumGroups = 1
DO UNTIL LEN(Amt) < 1
        IF LEN(Amt) >= 3 THEN Chunk = RIGHT$(Amt, 3)

        IF LEN(Amt) <= 2 THEN Chunk = Amt

        ' // RIGHTMOST character //
        SVal = MID$(Chunk, LEN(Chunk), 1)
        Value = VAL(SVal)
        ChunkVal = Numeral(1, Value) + ChunkVal
        OnesValue = Value
        IF LEN(Chunk) = 1 THEN GOTO EndChunk
                
        ' // MIDDLE character //

        SVal = MID$(Chunk, LEN(Chunk) - 1, 1)
        Value = VAL(SVal)
        TensValue = Value
        IF OnesValue <> 0 AND Value <> 0 THEN ChunkVal = "-" + ChunkVal
        ChunkVal = Numeral(2, Value) + ChunkVal
        IF LEFT$(ChunkVal, 5) = "TEEN-" THEN ChunkVal = Teen(OnesValue)
        IF LEN(Chunk) = 2 THEN GOTO EndChunk

        ' // LEFTMOST character //

        SVal = MID$(Chunk, LEN(Chunk) - 2, 1)
        Value = VAL(SVal)
        IF Value = 0 THEN
                IF OnesValue = 0 AND TensValue = 0 THEN
                        GOTO AllZero
                ELSE
                        GOTO EndChunk
                        END IF
                END IF

        ChunkVal = Numeral(1, Value) + " hundred " + ChunkVal
        IF OnesValue = 0 AND TensValue = 0 THEN
                ChunkVal = Numeral(1, Value) + " hundred"
                END IF

EndChunk:
        ChunkVal = ChunkVal + SPACE$(1) + GroupingAppend(NumGroups) + ","
        WholeValue = ChunkVal + SPACE$(1) + WholeValue

AllZero:
        Value = LEN(Amt)
        IF Value >= 3 THEN Amt = LEFT$(Amt, LEN(Amt) - 3)
        IF Value <= 2 THEN Amt = ""


        NumGroups = NumGroups + 1
        ChunkVal = ""
        Value = 0

LOOP

Note that while it takes the groups of three digits one at a time, it takes the digits themselves one at a time as well; in fact, there are separate lines of code for the RIGHTMOST character, the MIDDLE character, and the LEFTMOST character. This merely imitates the way humans read digits in.

Notice also that on many of the places, attention is paid to formatting the WholeValue with commas, the word "and", and spacing. We wanted to keep the Numeral library as free of spaces as possible. (The obvious exception would be Numeral(2, 0); see above for details.)

The last part of the algorithm is now ready (Steps 5 and 6). We are to clean up the WholeValue string, combine the two strings (DecimValue and WholeValue), format them so that they fit into each other, and then put them into the return value of the function.

Here is the last part of the Checkese function code.

 
DO WHILE RIGHT$(WholeValue, 1) = SPACE$(1) OR RIGHT$(WholeValue, 1) = ","
        WholeValue = LEFT$(WholeValue, LEN(WholeValue) - 1)
LOOP

MID$(WholeValue, 1, 1) = UCASE$(LEFT$(WholeValue, 1))

Checkese$ = WholeValue + SPACE$(1) + "and" + SPACE$(1) + DecimValue

And there you go. The resulting string is assigned to the Function itself, and is returned by the function to the program.

Quality Control

Of course, as with any imperfect theory, the translation subroutine needed some "tweaking" around with before it would work out right.

For example, one thing I always encountered when I tested the routine were extra spaces and commas to the right of the output caused by extra formatting. I fixed this by using the LEFT$() code in the above code sample, which simply erased them.

Earlier in this article, we have said that "quadrillion" was enough for most professionals to work with. What if the user enters a string that goes beyond "hundreds of quadrillions"? We can trap for this incalculable error beforehand. A "hundred quadrillion" string, plus the extra two decimals, is worth 20 characters. So after all the variable declarations (before the procedure actually manipulates the string), we insert this conditional.

 
IF LEN(Amt) > 20 THEN
        Checkese$ = "ERROR"
        EXIT FUNCTION
        END IF

As can be seen, the value of the function becomes the string "ERROR". The program then exits the function. (If this trap is not introduced, values with more than 20 digits would produce a "Subscript out of range" error message anyway.)

What if this 20-digit stuff is not enough? What if we needed to calculate quintillions, sextillions, and so on? All we need to do is to add functionality to the program. We add an array value to GroupingAppend() and increase the trap amount by three for every GroupingAppend we add.

 
DIM GroupingAppend(1 TO 7) AS STRING
.
.
.
        GroupingAppend(7) = "quintillion"
.
.
.
IF Len(Amt) > 23 THEN

N.B. : This improvement was not made to the source code that comes with this program.

Some Improvement Challenges

Here is a list of things that you, the avid QBasic programmer, might want to do to improve on the algorithm.
    One of the simplest: can you make a program that uses this procedure to read in figures from a file and output the translations to another file? This can be done using the File-Handling routines already provided with QBasic.

    Is there any way to make the main translation subroutine take up less lines of code? I am bothered by this because most of the lines in this subroutine (Step 4) are "formatting" subroutines: they take out extra spaces, look up the TEEN array, and so on. Even if it were just an ingenious way to put all this formatting at the end of the subroutine, it would increase the readability of the code.

    Notice also on the translation subroutine, there are two line labels: EndChunk and AllZero. EndChunk is used to clean up all the temporary variables and AllZero is where the program goes when it wants to skip a three-digit chunk of zeroes. Is there any way of removing these line labels so that it can be more easily ported to C?

    Which takes me to the last challenge: can you port this procedure to C/C++? This time, it would be a function that takes a char array and that will return another char array.

Of course, if you have the answers to any of these challenges, contact me, math_wizard44@hotmail.com; or, better yet, publish it on the Basix Fanzine. Good luck!

Program Source

Here's the complete source code of the program, including a short demonstration of its abilities. Notice that I have added a "bonus" function: CheckeseCurrency$(). This just formats a Checkese-ready string into proper currency notation. (I think the code is self-explanatory; it is easy to figure out since it is derived from the original Checkese function itself. Have fun with the code. N.B.: The last DATA entry was supposed to demonstrate what happens if the string were too long.

 
DECLARE FUNCTION Checkese$ (Amount AS STRING)
DECLARE FUNCTION CheckeseCurrency$ (Amount AS STRING)

CLS

FOR x = 1 TO 25
        READ n$
        LOCATE 2, 1: PRINT SPACE$(80)
        LOCATE 3, 1: PRINT SPACE$(80)
        LOCATE 2, 1: PRINT CheckeseCurrency$(n$) + " > "
        LOCATE 3, 1: PRINT Checkese$(n$)
        LOCATE 25, 1: PRINT "Press any key to continue..."
        SLEEP
NEXT x

END

DATA 13, 763
DATA 8033, 12245
DATA 789003, 3594006
DATA 62095239, 104592350
DATA 1110439595, 93125687090
DATA 6593584958205, 5294582000042
DATA 23846309766532, 634687239587298
DATA 462980003246345, 5246287598729879
DATA 2104820002345283, 72358729798579285
DATA 46349820994868692, 100000006246724355
DATA 210104247600030246, 6348679872200902452
DATA 6049680935200023568, 10000500000004235235
DATA 346343735389470394583048530

FUNCTION Checkese$ (Amount AS STRING)

DIM Amt         AS STRING
DIM WholeValue  AS STRING
DIM DecimValue  AS STRING

DIM Chunk       AS STRING
DIM ChunkVal    AS STRING
DIM SVal        AS STRING

DIM Value       AS INTEGER
DIM OnesValue   AS INTEGER
DIM NumGroups   AS INTEGER

DIM GroupingAppend(1 TO 6) AS STRING

        GroupingAppend(1) = ""
        GroupingAppend(2) = "thousand"
        GroupingAppend(3) = "million"
        GroupingAppend(4) = "billion"
        GroupingAppend(5) = "trillion"
        GroupingAppend(6) = "quadrillion"
       
DIM Numeral(1 TO 2, 0 TO 9) AS STRING
        Numeral(1, 0) = "": Numeral(2, 0) = "and "
        Numeral(1, 1) = "one": Numeral(2, 1) = "TEEN-"

        Numeral(1, 2) = "two": Numeral(2, 2) = "twenty"
        Numeral(1, 3) = "three": Numeral(2, 3) = "thirty"
        Numeral(1, 4) = "four": Numeral(2, 4) = "forty"
        Numeral(1, 5) = "five": Numeral(2, 5) = "fifty"
        Numeral(1, 6) = "six": Numeral(2, 6) = "sixty"
        Numeral(1, 7) = "seven": Numeral(2, 7) = "seventy"
        Numeral(1, 8) = "eight": Numeral(2, 8) = "eighty"
        Numeral(1, 9) = "nine": Numeral(2, 9) = "ninety"

DIM Teen(0 TO 9) AS STRING
        Teen(0) = "ten"

        Teen(1) = "eleven"
        Teen(2) = "twelve"
        Teen(3) = "thirteen"
        Teen(4) = "fourteen"
        Teen(5) = "fifteen"
        Teen(6) = "sixteen"
        Teen(7) = "seventeen"
        Teen(8) = "eighteen"
        Teen(9) = "nineteen"

Amt = Amount

IF LEN(Amt) > 20 THEN

        Checkese$ = "ERROR"
        EXIT FUNCTION
        END IF

DO WHILE LEFT$(Amt, 1) = "0"
        Amt = RIGHT$(Amt, LEN(Amt) - 1)
LOOP

' // Get Decimals //

Chunk = RIGHT$(Amt, 2)
DecimValue = Chunk + "/100 dollars"
Amt = LEFT$(Amt, LEN(Amt) - 2)

IF LEN(Amt) = 0 OR VAL(Amt) = 0 THEN
        Checkese$ = DecimValue
        EXIT FUNCTION
        END IF

' // Get All Other Digits //

NumGroups = 1
DO UNTIL LEN(Amt) < 1
        IF LEN(Amt) >= 3 THEN Chunk = RIGHT$(Amt, 3)

        IF LEN(Amt) <= 2 THEN Chunk = Amt

        ' // RIGHTMOST character //
        SVal = MID$(Chunk, LEN(Chunk), 1)
        Value = VAL(SVal)
        ChunkVal = Numeral(1, Value) + ChunkVal
        OnesValue = Value
        IF LEN(Chunk) = 1 THEN GOTO EndChunk
                
        ' // MIDDLE character //

        SVal = MID$(Chunk, LEN(Chunk) - 1, 1)
        Value = VAL(SVal)
        TensValue = Value
        IF OnesValue <> 0 AND Value <> 0 THEN ChunkVal = "-" + ChunkVal
        ChunkVal = Numeral(2, Value) + ChunkVal
        IF LEFT$(ChunkVal, 5) = "TEEN-" THEN ChunkVal = Teen(OnesValue)
        IF LEN(Chunk) = 2 THEN GOTO EndChunk

        ' // LEFTMOST character //

        SVal = MID$(Chunk, LEN(Chunk) - 2, 1)
        Value = VAL(SVal)
        IF Value = 0 THEN
                IF OnesValue = 0 AND TensValue = 0 THEN
                        GOTO AllZero
                ELSE
                        GOTO EndChunk
                        END IF
                END IF

        ChunkVal = Numeral(1, Value) + " hundred " + ChunkVal
        IF OnesValue = 0 AND TensValue = 0 THEN
                ChunkVal = Numeral(1, Value) + " hundred"
                END IF

EndChunk:
        ChunkVal = ChunkVal + SPACE$(1) + GroupingAppend(NumGroups) + ","
        WholeValue = ChunkVal + SPACE$(1) + WholeValue

AllZero:
        Value = LEN(Amt)
        IF Value >= 3 THEN Amt = LEFT$(Amt, LEN(Amt) - 3)
        IF Value <= 2 THEN Amt = ""


        NumGroups = NumGroups + 1
        ChunkVal = ""
        Value = 0

LOOP

DO WHILE RIGHT$(WholeValue, 1) = SPACE$(1) OR RIGHT$(WholeValue, 1) = ","
        WholeValue = LEFT$(WholeValue, LEN(WholeValue) - 1)
LOOP

MID$(WholeValue, 1, 1) = UCASE$(LEFT$(WholeValue, 1))

Checkese$ = WholeValue + SPACE$(1) + "and" + SPACE$(1) + DecimValue

END FUNCTION

FUNCTION CheckeseCurrency$ (Amount AS STRING)

DIM Amt         AS STRING
DIM DecimValue  AS STRING
DIM WholeValue  AS STRING

DIM StringBuff  AS STRING

Amt = Amount

DO WHILE LEFT$(Amt, 1) = "0"
        Amt = RIGHT$(Amt, LEN(Amt) - 1)
LOOP

DecimValue = RIGHT$(Amt, 2)
Amt = LEFT$(Amt, LEN(Amt) - 2)

DO UNTIL LEN(Amt) < 3

        StringBuff = RIGHT$(Amt, 3)
        WholeValue = "," + StringBuff + WholeValue
        Amt = LEFT$(Amt, LEN(Amt) - 3)
LOOP

Amt = Amt + WholeValue + "." + DecimValue

DO WHILE LEFT$(Amt, 1) = "," OR LEFT$(Amt, 1) = " "
        Amt = RIGHT$(Amt, LEN(Amt) - 1)
LOOP

Amt = "$" + SPACE$(1) + Amt

CheckeseCurrency$ = Amt

END FUNCTION
  That's it for this article. If you would like to see me write more articles in the future, you can do one of two things: a) Send me an electronic message requesting me to write one, or b) post a request to the fine people at the Basix Fanzine.

Have fun coding!
 
 
Content of this article is partly ©2000 The Math Wizard. All rights reserved.
Use of this document for profit may infringe on the rights of its owners.





This article originally appeared in The BASIX Fanzine Issue 17 from April 2000.