Welcome To The Home Of The Visual FoxPro Experts  
home. signup. forum. archives. search. google. articles. downloads. faq. members. weblogs. file info. rss. print.
TAME THE STRING

To be honest, even long after I passed my computer courses, I still had lots of program exercises left unsolved. Don’t know why my professors allowed me to pass those subjects. I only had the chance to look into these exercises in an “intimate level” during my first employment, sitting in front of a computer terminal each day wrestling with trial and error.

I guess it’s due to the fact that programming is a skill and in order to have THE sufficient skill, one must constantly practice the craft until he “feels” the craft. I remember a moment (this is a real-life experience, folks, and I’m not kidding) when my wife and I had once a soft argument. She’s doing all the repetitious blah-blahs and I was just holding my head low, maintaining my silence when she asked, “Why aren’t you speaking up?” I said, “I already did but I don’t want to say it again. Can’t you see that one word is enough for a wise man?” Much to my surprise when she replied, “Well, can’t you see that practice makes perfect?!”

Back in my college days, we did not have a one-computer-per-student setup. PCs were too expensive, or to put it clearly, my parents could not afford to buy one. Our schedules were only a couple of hours, thrice a week in the school’s computer lab. Those were the only times that I could had the chance to feel the touch of the keyboard, when I got lucky – that was, when I beat the rest of my classmates in a race that started from the lab’s door to one of those clunky desktops. Most often than not, after the class I left the lab with a “midnight” syndrome – everything about the subject was pitch dark in my head. I could not ask questions during the class because I didn’t know what to ask.



Whether you are still dreaming of becoming a hardcore developer someday or you are already a respectable hardcore, we all have our humble beginnings. So this article is intended for those new to Visual FoxPro. Maybe the neophyte in you had once entertained a question, “Where would I start?”

I always advise newcomers to FoxPro (or to any development tool, to that respect) to master the art of character string manipulation. Why character? Why not other data types? Firstly, didn’t we once love the dreaded and infamous ”Hello, world!”? Secondly, character string is the easiest data type to deal with. You can chop it, chain it, reverse it, toss it upside down, even “marcott” it. String manipulation promises a good foundation for logical exercises. After all, programming is about logic --- not mathematics, although, being darn good at math is a big plus. My point is, when you know the ins and outs of character string, the rest will follow suit: dates, numerics, arrays and other bits and pieces until you find yourself writing a sizeable application. Lucky for us, FoxPro has rich and extensive string functions in its arsenal.

I won’t discuss all string functions here to avoid redundancy since they are all documented in VFP help file. If you are using VFP 5, open Help, Help Topics, Contents, Language Reference, Language Categories, Data Types and Character Functions. In VFP 6, open the MSDN library for Visual Studio 6, Visual FoxPro Documentation, Reference, Language Overview, Language Categories, Data Types and Character Functions.

What I’m about to present here are two common utilities -- that employed and exploited VFP’s string functions -- written to solve our common programming woes. I won’t go into details regarding the syntaxes of the string functions that I used in the succeeding examples. It’s for you to study and to discover why I used them in particular fashion unless I explicitly stated herein in order to expand my views on the subject.

The first utility is my own replacement for the Str() function of VFP. Although, I mentioned that VFP has plenty of string functions, personally, I think Str() sucks a bit. It has quirks especially in handling decimals. You have to pass the width of the entire value and the intended width for the decimals for the Str() to convert the numeric value to character correctly.

What’s the syntax of Str() again? Str(lnNumericValue,[lnEntireWidth,[lnDecimalWidth]])

If lnDecimalWidth is greater than the length of the decimals of lnNumericValue, Str() replaces the extra spaces on the right with zeroes – which is quite unnecessary. We have real-life computations at times that the number of digits of decimal values changes at run-time. Tracking and re-setting the value of Set(“Decimals”) are rather an inconvenient approach. Whoa! Wait a minute! Are we still talking about character string? You bet and we are talking conversion here.

I would like to note, also, that VFP could only accommodate 17 digits all in all (integers or real numbers) including the decimal dot. Above 17 -- values will be rounded. Above 19 -- values will be converted to scientific notation. However, Str() in particular accepts up to 18 decimal digits --- yet the result will still be rounded. Above 18 in decimals will drive the function against the wall.

So you can pass parameters like this: Str(lnNumValue,40,18)

But this will ka-vooom!: Str(lnNumValue,40,19)

Please refer to Figure 1 to see another interesting observation and if you can spot a notable difference.



What about in Figure 2?


So let’s have an alternative to the Str() function but will have to stick to 12-decimal digits, I’m afraid, to stay on a safe ground. Let’s name our function as… well, what else is more apt than… BetterStr()?

BetterStr(): Better Than the Bittered Str()
Pass two parameters to BetterStr(). One is a required numeric value and the other is an optional delimiter character string as opposed to Str()’s parameters – one required, two optional.

Figure 3 illustrates the comparative results of BetterStr() and Str().


Here is the code:
FUNCTION BetterStr(lnNumber,lcDelimiter)
	LOCAL lnX,lnLenString,lnZeroCount,lnInteger,lnDecimal,lcNumber,lcInteger
	LOCAL lcFormat, lcNumber = ""
	IF TYPE('lnNumber') = "N"
		lnInteger = INT(lnNumber)
		lnDecimal = lnNumber - lnInteger
		lcNumber = ALLTRIM(STR(lnInteger,18,0)) + ALLTRIM(STR(lnDecimal,12,12))
		lnLenString = LEN(lcNumber)
		lnZeroCount = 0
		FOR lnX = lnLenString TO 1 STEP -1
			IF SUBSTR(lcNumber,lnX,1) = "0"
				lnZeroCount = lnZeroCount + IIF(SUBSTR(lcNumber,lnX,1) = "0",1,0)
			ELSE
				EXIT
			ENDIF
		NEXT
		lcNumber = LEFT(lcNumber,lnLenString - lnZeroCount)
		lcNumber = IIF(RIGHT(lcNumber,1) = ".",;
			LEFT(lcNumber,LEN(lcNumber) - 1),lcNumber)
		IF TYPE('lcDelimiter') = "C"
			lnLenString = LEN(ALLTRIM(STR(INT(VAL(lcNumber)),30)))
			IF lnLenString > 16
				lcInteger= ALLTRIM(STR(INT(VAL(lcNumber))))
			ELSE
				 lcFormat = ""
				 lnZeroCount = 0 &&recycled lnZeroCount var, not a good practice
				 FOR lnX = lnLenString TO 1 STEP -1
				lnZeroCount = IIF(lnZeroCount < 4,lnZeroCount + 1,1)
				lcFormat = IIF(lnZeroCount = 4,lcDelimiter,'9') + lcFormat
				lnX = lnX + IIF(lnZeroCount = 4, 1, 0)
				 NEXT
				 lcInteger = TRANSFORM(INT(VAL(lcNumber)),lcFormat)
			ENDIF
			lcNumber = lcInteger + IIF(AT(".",lcNumber) > 0, SUBSTR(lcNumber,AT(".",lcNumber)),"")
		ENDIF
	ENDIF
	RETURN lcNumber
ENDFUNC


The second utility is about converting numeric value to English word. Much like a check writer. Pity that VFP has no built-in function such as this. My first ever Windows-based program was the buggy dBFast from Computer Associates and the authors included a similar function way back then in 1993.
You might have already seen this utility in textbooks or from other resource materials. Anyway, I added tiny innovations here and there to drive your interest high. It accepts over 999 billion (negative and positive) amount with an optional second parameter for the currency unit. The default unit is Peso – Philippine currency. You can pass “Dollar” or “Yen” to replace the default value or simply pass an empty string “ “ to suppress the display of the currency unit. Since we are dealing with money here, decimal value is truncated into two digits only.


Figure 4 below showcases what you can do with Num2Words() function.


FUNCTION Num2Words(lnNumeral,lcCurrency)
	LOCAL lnInteger,lnDecimal,lcNum2Str,lcString1,lcString2
	LOCAL lcString3,lcReturnStr,lnX,lnWidthStr,lcSegment,lnIncrement
	LOCAL lcCluster,lnOrigLen
	IF TYPE('lnNumeral') # "N"
		RETURN ""
	ENDIF
	lcCurrency = IIF(TYPE('lcCurrency') # "C","Peso(s)",lcCurrency)
	lnInteger = INT(lnNumeral)
	lnDecimal = lnNumeral - lnInteger
	IF lnInteger = 0 AND lnDecimal = 0
		RETURN "Zero"
	ENDIF
	lcNum2Str = ALLTRIM(BetterStr(lnInteger))
	IF !BETWEEN(lnInteger,-999999999999.99,999999999999.99)
		RETURN REPLICATE('*',LEN(lcNum2Str))
	ENDIF
	lcString1 = "     One  Two  ThreeFour Five Six  SevenEightNine "
	lcString2 = "Ten      Eleven   Twelve   Thirteen Fourteen Fifteen  " + ;
		"Sixteen  SeventeenEighteen Nineteen "
	lcString3 = "       Twenty Thirty Forty  Fifty  Sixty  SeventyEighty Ninety "
	STORE LEN(lcNum2Str) TO lnWidthStr,lnOrigLen
	lcReturnStr = IIF(lnInteger < 0, "Negative ","")
	DO WHILE lnWidthStr > 0 AND lnInteger # 0
		lnX = MOD(lnWidthStr,3)
		lnX = IIF(lnX = 0,3,lnX)
		lcCluster = ALLTRIM(LEFT(lcNum2Str,lnX))
		FOR lnIncrement = 1 TO lnX
			lcSegment = ALLTRIM(SUBSTR(lcCluster,lnIncrement,3))
			IF LEN(lcSegment) # 2
				lcReturnStr = ALLTRIM(lcReturnStr) + " " + ;
					ALLTRIM(SUBSTR(lcString1,VAL(LEFT(lcSegment,1)) * 5 + 1,5))
				IF LEN(lcSegment) = 3 AND VAL(LEFT(lcSegment,1)) > 0
					lcReturnStr = ALLTRIM(lcReturnStr) + " Hundred"
				ENDIF
			ELSE
				IF VAL(LEFT(lcSegment,1)) < 2
					IF VAL(LEFT(lcSegment,2)) > 9
						lcReturnStr = ALLTRIM(lcReturnStr) + " " + ;
							ALLTRIM(SUBSTR(lcString2,;
							VAL(RIGHT(lcSegment,1)) * 9 + 1,9))
						EXIT
					ENDIF
				ELSE
					lcReturnStr = ALLTRIM(lcReturnStr) + " " + ;
						ALLTRIM(SUBSTR(lcString3,;
						(VAL(LEFT(lcSegment,1))-1) * 7 + 1,7))
					IF RIGHT(lcSegment,1) = "0"
						EXIT
					ENDIF
				ENDIF
			ENDIF
		NEXT
		DO CASE
			CASE LEN(lcNum2Str) > 9 AND VAL(lcCluster) # 0
				lcReturnStr = ALLTRIM(lcReturnStr) + " Billion"
			CASE LEN(lcNum2Str) > 6 AND VAL(lcCluster) # 0
				lcReturnStr = ALLTRIM(lcReturnStr) + " Million"
			CASE LEN(lcNum2Str) > 3 AND VAL(lcCluster) # 0
				lcReturnStr = ALLTRIM(lcReturnStr) + " Thousand"
		ENDCASE
		lnWidthStr = lnWidthStr - lnX
		lcNum2Str = RIGHT(lcNum2Str,lnWidthStr)
	ENDDO
	lcReturnStr = ALLTRIM(lcReturnStr) + " " + ;
		IIF(EMPTY(lcReturnStr),"",lcCurrency)
	IF lnDecimal # 0.00
		lcReturnStr = lcReturnStr + IIF(lnInteger = 0 AND;
			EMPTY(ALLTRIM(lcReturnStr)),"Negative "," And ")
		lcReturnStr = lcReturnStr + ;
			ALLTRIM(STR(ABS(lnDecimal * 100),2,2)) + "/100 Cents"
	ENDIF
	RETURN ALLTRIM(lcReturnStr)
ENDFUNC


Fox And Theories
Regarding the Num2Words(), you can change the returned value to conform your native language. The trick lies in the three variables lcString1, lcString2 and lcString3 and also in the extraction of the correct string. Obviously, lcString1 holds the ones, lcString2 holds the tens and lcString3 holds the tens that end with “ty” as in Twenty, Thirty, so on.
The strings are chained and each individual’s width is fixed based on the item that has the most characters. Let’s take for example, in ones: the longest are Three, Seven and Eight – with 5 characters each. To illustrate further, let’s tabulate each character:




1

2

3

4

5

1

2

3

4

5

1

2

3

4

5

1

2

3

4

5

1

2

3

4

5


 
 
 
 
 
O
n
e
 
 
T
w
o
 
 
T
h
r
e
e
F
o
u
r
 



The first five characters are intended for “Zero” but we placed the code to trap zero value in the early part of the list so that VFP won’t have to undergo the gory details just to return “Zero”.
Let’s say, we passed the value 2 which is converted to “2” by BetterStr(). Along the way it has become part of lcSegment variable. The following code snippet extracts “Two” from lcString1:
AllTrim(SubStr(lcString1,Val(Left(lcSegment,1)) * 5 + 1,5))

First, Val() converts “2” back to 2 so that we can establish our equation. We multiplied it by 5, our fixed width. We need to move our pointer right in the beginning of the string we are about to extract that’s why we add 1. The result is 11. If you count 11 boxes from the left to the right, our pointer stops at “T”. This is where the SubStr() function starts its role by counting another 5 characters from the pointer. AllTrim() trims any leading or trailing blanks from the value returned by SubStr().
One catch: Don’t use this: Num2Words(-). If you know how to amend this logical lapse without using ON ERROR, please let me know.

Before I Retreat Graciously Towards My Foxhole…
You may optimize the above codes to your heart’s content but I would appreciate much if you remember me by sending a copy of your improved version. Deal?

I once read a phrase but never had the chance to know the writer’s name. I must have taken it in one of those Hallmark bookmarkers – I don’t know, I already forgot. But I keep the message with me through the years in my heart, er, in my mind, I suppose. It’s beautiful that I’m sure only somebody who had experienced a knowledge transformation could only write. It goes, “Man’s mind once stretched by a new idea never regains its original dimension”.

Master the string manipulation. When you do, you’ll never be the same again.

Download code
Click here to download the code that is discussed in this article. The download is a zipfile. Its size is 1.593 bytes.

ABOUT THE AUTHOR: DALE DEDOROY

Dale Dedoroy Dale is a poor boy who finds joy in programming. He is currently the I.T. Officer of C.W. Mosser Environment Corporation.

FEEDBACK

Harsh @ 1/23/2008 11:27:46 AM
Hello Dale,Thanks for providing Free resource for converting numeric to
englsh Words,

However from your downladed programe I am unable to get where we have to put value to get the output. I mean where I have to give the input .ie which variable.

Mail me :

harshfatnani@rediffmail.com

sandy @ 4/12/2008 7:55:07 AM
Hi Dale Thanks dear for this free ware

gtherukunnel@rediffmail.com @ 6/3/2008 7:46:03 AM
pl. find athere below a code to extract words out of figure in foxpro.

SET TALK OFF
N1 = "One"
N2 = "Two"
N3 = "Three"
N4 = "Four"
N5 = "Five"
N6 = "Six"
N7 = "Seven"
N8 = "Eight"
N9 = "Nine"
N10 = "Ten"
N11 = "Eleven"
N12 = "Twelve"
N13 = "Thirteen"
N14 = "Fourteen"
N15 = "Fifteen"
N16 = "Sixteen"
N17 = "Seventeen"
N18 = "Eighteen"
N19 = "Nineteen"
N20 = "Twenty"
N30 = "Thirty"
N40 = "Forty"
N50 = "Fifty"
N60 = "Sixty"
N70 = "Seventy"
N80 = "Eighty"
N90 = "Ninety"

Times = 1
Begin = 1
Words = " "

Input "Enter any Number :" to Num
String = STR(Num,9,2)

*Loop through thousands and hundreds

DO WHILE Times <3
*Split out Hundres, tens and ones

Balance=SUBSTR(String,Begin,3)
Hun =SUBSTR(Balance,1,1)
Ten =SUBSTR(Balance,2,2)
One =SUBSTR(Balance,3,1)

*Handle hundreds portion

IF VAL(Balance)>99
Words = Words+" "+ N&Hun+" "+'Hundred'
Endif

*Handle second two digits

T=VAL(Ten)
IF T >0
* Handle even tens and teens
IF (INT(T/10.0)=T/10.0) or (T>9 and T<20)
Words = Words+" " +N&Ten

* When the number is greater than 10, but not evenly divisible.
ELSE
IF T>9 and (INT(T/10.0) # T/10.0)
Ten=SUBSTR(Ten,1,1)+ '0'
Words = Words+" "+ N&Ten+' '+ N&One

* When the number is less than 10
ELSE
IF T < 10
Words = Words+" "+N&One
ENDIF
ENDIF
ENDIF
ENDIF

*If number is greater that 999.99 add "Thousand"

IF Num > 999.99 and Times=1
Words = Words+" "+'Thousand'
ENDIF

* Prepare for pass through hundreds

Begin=4
Times = Times+1
ENDDO

*hANDLE DECIMALS

IF (Num-INT(Num))<>0
IF INT(Num) >0
Words = "Words"+' '+'and'
ENDIF
Words = "Rupees "+Words+" "+SUBSTR(String,8,2)+" "+"Paise"+' Only'
ENDIF

? Words

Lopera 19 @ 7/15/2008 4:26:38 AM
Thanks! This is very helpful to me since I am new to Foxpro. I don't know if I'll be an expert someday but for me, endurance is the key.

Foxpro-newbie @ 3/30/2009 4:15:39 PM
T/10.0 Why did you divide the T to 10.0? What does it mean?

joecoder @ 10/30/2009 6:51:58 PM
Good stuff. Very helpful! Thanks Dale



Your Name: 
Your Feedback: 

Spam Protection:
Enter the code shown: