Monday, 1 June 2026

NumberToWords

 ========================================================================

LESSON: MODULARIZATION, DEFENSIVE CODING, AND STATE DETERMINISM IN VFP

========================================================================


Today we are looking at how to take legacy procedural code—specifically a numeric-to-words parsing routine—and refactor it into clean, maintainable, decoupled architecture using Visual FoxPro. 

Review the architecture below, paying close attention to the design patterns, array usage, and standard VFP scoping rules.

------------------------------------------------------------------------

RULE 1: STRICT SCOPING IN LPARAMETERS WITH THE "m." PREFIX

------------------------------------------------------------------------

In Visual FoxPro, you MUST use the "m." prefix on all memory variables when declaring them inside LPARAMETERS statements. 

Why? If a database table is open and contains a field name identical to your parameter variable, FoxPro will prioritize the table field over your variable during execution. Omitting "m." in the parameter list introduces silent, devastating bugs that surface or disappear based entirely on runtime work area states. 

NOTE ON LOCAL DECLARATIONS: You may exclude the "m." prefix inside standard 

LOCAL declarations (e.g., LOCAL lcLeftPart) because LOCAL explicitly forces the creation of a memory variable placeholder. However, using "m." during the subsequent assignment and evaluation of those variables remains standard practice.

EXCEPTION: Do not use "m." when referencing or passing ARRAYS (including array parameters in LPARAMETERS). FoxPro treats array names uniquely, and attaching the "m." prefix will trigger syntax errors during array manipulation or pass-by-reference operations.

------------------------------------------------------------------------

RULE 2: STATE DETERMINISM AND ZERO REDUNDANT ALLOCATIONS

------------------------------------------------------------------------

Avoid double-assignments like this:

    lcResult = ""

    IF m.lnCondition > 0

        lcResult = "Value"

    ENDIF


When the condition is true, the CPU performs two sequential writes to the same memory space. Instead, use an explicit IF/ELSE block:

    IF m.lnCondition > 0

        lcResult = "Value"

    ELSE

        lcResult = ""

    ENDIF

This enforces a clean state, makes code highly readable, and eliminates wasted CPU cycles on redundant string allocations. Note that any variablesevaluated within the condition block (like m.lnCondition) strictly use the "m." prefix to satisfy Rule 1.


========================================================================

THE REFACTORED MODULES (STANDALONE .PRG FILES)

The benefit is these will get added to your project or exe just by calling them from your code.

========================================================================


--- FILE 1: numtowords.prg ---

* Main execution entry point

LPARAMETERS m.tnNumericValue


* Guard clause: Validate input immediately before allocating locals

IF EMPTY(m.tnNumericValue) OR NOT INLIST(VARTYPE(m.tnNumericValue), "N", "I", "Y")

    RETURN ""

ENDIF


LOCAL lcLeftPart, lnLeftSize, lnReiterate, lnInvar, ;

      lcNumericValue, lnInmove, lcTaken, lcConcatenate, ;

      lcWordsext, lcNewword, lnRval

      

LOCAL ARRAY laWords[6], laOnes[9], laTeens[10], laTens[9], laScales[5]


* Populate static lookup arrays via function-style call with pass-by-reference

=populatearrays(@laOnes, @laTeens, @laTens, @laScales)


lcLeftPart = ALLTRIM(TRANSFORM(INT(VAL(TRANSFORM(m.tnNumericValue)))))

lnLeftSize = LEN(m.lcLeftPart)


* Calculate how many 3-digit blocks exist beyond the first block

lnReiterate = INT((m.lnLeftSize - 1) / 3)


lnInmove = m.lnReiterate * 3

lcNumericValue = ALLTRIM(RIGHT(m.lcLeftPart, m.lnInmove))


* 1. Process the leftmost remaining block (1 to 3 digits)

lcConcatenate = getleadingblockword(m.lcLeftPart, m.lnLeftSize, m.lnInmove)


* 2. Process subsequent 3-digit chunks moving left-to-right

FOR lnInvar = 1 TO m.lnReiterate

    lnInmove = m.lnInmove - 3

    lcTaken  = SUBSTR(m.lcNumericValue, m.lnInmove + 1, 3)

    

    lcConcatenate = getthreedigitword(m.lcTaken)

    

    IF !EMPTY(m.lcConcatenate)

        laWords[m.lnInvar] = m.lcConcatenate + " " + getwordfromlist(@laScales, m.lnInvar, 4)

    ELSE

        laWords[m.lnInvar] = ""

    ENDIF

ENDFOR


* 3. Assemble chunks and filter out empty scale names

lcWordsext = ALLTRIM(m.lcConcatenate)

FOR lnRval = m.lnReiterate TO 1 STEP -1

    IF !EMPTY(laWords[m.lnRval])

        IF GETWORDCOUNT(laWords[m.lnRval]) = 1 AND INLIST(UPPER(ALLTRIM(laWords[m.lnRval])), 'THOUSAND', 'MILLION', 'BILLION', 'TRILLION', 'QUADRILLION')

            LOOP

        ENDIF

        lcWordsext = m.lcWordsext + " " + laWords[m.lnRval]

    ENDIF

ENDFOR


lcNewword = ALLTRIM(m.lcWordsext)

RETURN m.lcNewword



--- FILE 2: populatearrays.prg ---

* Notice: Arrays are the explicit exception to the m. rule in parameter lists

LPARAMETERS taOnes, taTeens, taTens, taScales


* Ones (Indexes 1-9)

taOnes[1] = "One"

taOnes[2] = "Two"

taOnes[3] = "Three"

taOnes[4] = "Four"

taOnes[5] = "Five"

taOnes[6] = "Six"

taOnes[7] = "Seven"

taOnes[8] = "Eight"

taOnes[9] = "Nine"


* Teens (Values 10-19 mapped to Array Indexes 1-10)

taTeens[1]  = "Ten"

taTeens[2]  = "Eleven"

taTeens[3]  = "Twelve"

taTeens[4]  = "Thirteen"

taTeens[5]  = "Fourteen"

taTeens[6]  = "Fifteen"

taTeens[7]  = "Sixteen"

taTeens[8]  = "Seventeen"

taTeens[9]  = "Eighteen"

taTeens[10] = "Nineteen"


* Tens (Values 20-90 mapped to Array Indexes 1-9 natively)

taTens[1] = ""

taTens[2] = "Twenty"

taTens[3] = "Thirty"

taTens[4] = "Forty"

taTens[5] = "Fifty"

taTens[6] = "Sixty"

taTens[7] = "Seventy"

taTens[8] = "Eighty"

taTens[9] = "Ninety"


* Scale Titles (Indexes 1-5)

taScales[1] = "Thousand"

taScales[2] = "Million"

taScales[3] = "Billion"

taScales[4] = "Trillion"

taScales[5] = "Quadrillion"


RETURN



--- FILE 3: getwordfromlist.prg ---

LPARAMETERS taTargetArray, m.tnIndex, m.tnListType


DO CASE

    CASE m.tnListType = 2 && Teens offset mapping logic (10-19 -> Index 1-10)

        RETURN taTargetArray[m.tnIndex - 9]

    OTHERWISE

        RETURN taTargetArray[m.tnIndex]

ENDCASE



--- FILE 4: getleadingblockword.prg ---

LPARAMETERS m.tcLeftPart, m.tnLeftSize, m.tnInmove


LOCAL lnTargetLen, lcFirstChunk


lnTargetLen  = m.tnLeftSize - m.tnInmove

lcFirstChunk = LEFT(m.tcLeftPart, m.lnTargetLen)

RETURN getthreedigitword(m.lcFirstChunk)



--- FILE 5: getthreedigitword.prg ---

LPARAMETERS m.tcChunk


LOCAL lnVal, lnHundreds, lnRemainder, lcResult, lnTens, lnOnes


lnVal = VAL(m.tcChunk)

IF m.lnVal = 0

    RETURN ""

ENDIF


lnHundreds  = INT(m.lnVal / 100)

lnRemainder = m.lnVal % 100


* Handle Hundreds Place with deterministic assignment

IF m.lnHundreds > 0

    lcResult = getwordfromlist(@laOnes, m.lnHundreds, 1) + " Hundred"

ELSE

    lcResult = ""

ENDIF


* Handle Tens and Ones Places

IF m.lnRemainder > 0

    IF !EMPTY(m.lcResult)

        lcResult = m.lcResult + " "

    ENDIF

    

    DO CASE

        CASE m.lnRemainder < 10

            lcResult = m.lcResult + getwordfromlist(@laOnes, m.lnRemainder, 1)

        CASE m.lnRemainder >= 10 AND m.lnRemainder < 20

            lcResult = m.lcResult + getwordfromlist(@laTeens, m.lnRemainder, 2)

        OTHERWISE

            lnTens = INT(m.lnRemainder / 10)

            lnOnes = m.lnRemainder % 10

            lcResult = m.lcResult + getwordfromlist(@laTens, m.lnTens, 3)

            IF m.lnOnes > 0

                lcResult = m.lcResult + " " + getwordfromlist(@laOnes, m.lnOnes, 1)

            ENDIF

    ENDCASE

ENDIF


RETURN m.lcResult

========================================================================


No comments:

Post a Comment