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

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


Tuesday, 29 October 2013

What software should be and isn't

I've been involved in software for over 45 years and I've seen an awful lot that is awful. The question is, what to do about it? The point of the posts on this blog will be to examine some problems and propose solutions for them.

First off, I am not an example of Dunning Kruger. 

The DunningKruger effect is a hypothetical cognitive bias stating that people with low ability at a task overestimate their ability.

I have proof of my ability and a long history of delivering exceptional results. I repeatedly take even the very latest code from people with 15 years of experience, code that took 10 hours to run, and make it run in a few minutes, without even trying hard. That is because I don't write slow code at all. Learn as much about the tools as you can. I had an employer spend 2 weeks doing something. When he asked me to look at it, I redid the same effect after only a few minutes and he got really angry.

This is the level of some employers and programmers. They are so unfamiliar with their tools, that they can be shown up easily. That should not be possible among members of a "profession".

Here's the first one. "Software as art." Software may certainly involve that which may be artistic, but it should be engineering. First reason - companies want predictability. If your system is 80% CRUD operations and 20% invention of new algorithms, it will be possible to estimate. However, the more inventions that are required, the less likely the estimate will be valid. If every line is custom, or you have the same code in multiple places, it will become the proverbial rat's nest.

The reason so many office buildings are rectangular is because that meets the need. To make an office building into a big donut is adding artistic flair to something architectural. Most software seems to me to be barely architectural and that is really unfortunate. Here are a couple of examples of what passes as art and why software developers have to distance themselves from art.

http://www.youtube.com/watch?v=He7Ge7Sogrk

An elephant can paint. Can an elephant program? Possibly. Can an elephant invent painting? I doubt it. Yet, I think we can all agree the elephant created "art". Can an elephant imagine a machine and break the design down into components and sub-components and then build a machine with said components? I really doubt it.

However, if a programmer sits down and just writes out an entire ream of code without any of the recommended architectural practices - modularization, separation of concerns, etc., are they artist or engineer? There is a lot of similarity between writing out some code and what the elephant did. Take a tool, apply it, voila. Art.

http://www.dailymail.co.uk/femail/article-2325544/Millie-Brown-Vomit-Painter-pukes-canvas-create-Jackson-Pollock-style-art-Lady-Gaga-loves.html

I don't know about you, but if the above qualifies as art, I'd much rather be an engineer. I suggest programmers become familiar with software engineering basics, because more often than not, I've seen 'art'.

Don Knuth wrote a book called The Art of Programming. That does not prove programming is an art as this book is a treatise on how to be scientific.

If you are asked to write a piece of code to count the number of weekdays between any two dates and you offer a counting routine, that is what any 8 year-old literally can do. You as a programmer should not be proud of that solution. Some form of 5/7 of the total number of days, less offsets should be the answer of an engineer.

The loop solution is O(n). The 5/7 solution in O(1). 

Why are so many systems difficult or impossible to modify? The cause is "art". Engineers make it easy to maintain and extend their creations. Artists make their creations and pat themselves on the shoulder for their creativity. Program as if your successor is a homicidal maniac and knows where you live.