========================================================================
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
========================================================================