Вы находитесь на странице: 1из 13

Main: 52781 Holly Court (574) 273-5805 Production: 42967 Calle Reva (951) 693-4741

South Bend, IN 46637 sales@excelisys.com Temecula, CA 92592 www.excelisys.com

Recursive Calculations
in FileMaker Developer 7
by Andrew Persons FileMaker Developer 7s Custom Functions feature is a terrific tool with several advantages: code reusability, simplified code maintenance, and code sharing, among others. Its most valuable contribution, however, may be the ability to create looping calculations, in the form of recursive calculations. Among the plethora of newly introduced features, this could prove to be the most underrated, yet most powerful. Be prepared to boldly go where no calc has gone before. For this discussion, we will take a look at three examples Date Ranges Portal Filtering Phone Formatting

we will use the following approach in creating our recursive calculations 1. Start at the end 2. Drill down to the beginning 3. Work your way back up and our functions will all include the following essential elements 1. Exit Condition (what will end the loop) 2. Decrementing (or Incrementing) Parameters (what you pass to each new iteration) 3. Exit Statement (what the final iteration will perform)

What are recursive calculations?


DateRange1 ( StartDate ; EndDate ) =

Page 1 of 13

2004 Andrew Persons. All rights reserved.

Case ( EndDate > StartDate ; DateRange1 ( StartDate; EndDate - 1) & "" & EndDate ; EndDate)

Youll notice that the function is named DateRange1(), but DateRange1() also appears in its definition. But I thought FileMaker wouldnt allow you to do that, since it would create a looping definition, you say, and youre right prior to FileMaker 7. FileMaker Developer 7 introduces Custom Functions, which allow you to define your own calculations once, and use them many times. It also allows a Custom Function to call itself. When a calculation does this, it is known as recursion. A recursive expression is a function that loops back to the beginning of itself until it detects that a certain condition has been satisfied. A recursive calculation calls itself (loops back to the beginning of itself) until some condition has been satisfied. This is the key. If a calculation simply called itself, it would loop indefinitely. (In FileMaker 7, it would loop 10,000 times or until FileMaker ran out of memory, whichever happened first.) We have to give it the ability to exit the loop. To illustrate this, lets look at our first example:

Example 1: DateRange1()
DateRange1 ( StartDate ; EndDate ) = Case ( EndDate > StartDate ; DateRange1 ( StartDate; EndDate - 1) & "" & EndDate ; EndDate)

This function takes two parameters, StartDate and EndDate, and produces a returndelimited list of all the dates in between, inclusively. It starts with a case statement:
Case ( EndDate > StartDate

OK, so this will only execute if EndDate is later than StartDate. But we already know that the user will probably be entering an end date that is later than the start date, so what is the point? Heres where it gets interesting; this is what gets executed if the case statement conditions are met:
DateRange1 ( StartDate; EndDate - 1) & "" & EndDate

It calls DateRange1() again, and passes the same parameters again, almost. The difference is it decrements EndDate by 1. Lets pause for a moment and define a term, iteration. Every time a function, such as DateRange1() is called, its referred to as an iteration. If it loops 10 times, it has 10 iterations. Now back to our regularly scheduled programming.

Page 2 of 13

2004 Andrew Persons. All rights reserved.

What happens now is that the first iteration of DateRange1() pauses while a new iteration, with new parameters, is called. This new iteration has the original StartDate passed to it, but EndDate is now one day earlier. The new iteration has no awareness that it has been called by a previous iteration: as far as its concerned, its the only iteration in the universe (iterations are very self-involved). Now, the new iteration starts over, and calls the case statement EndDate > StartDate. If it is still true, it will call yet another iteration, with EndDate reduced by 1 again. This will continue until EndDate = StartDate. At this point, the case statement will fail, and the second option will be executed:
EndDate

The iteration will simply return EndDate, and return to the previous iteration. If youll recall, we were here
DateRange1 ( StartDate; EndDate - 1) & "" & EndDate

when the new iteration was called. That iteration returned EndDate; now we proceed to add a return and our EndDate, which is quite different from the other iterations EndDate. This iteration will return two dates separated by a return to the iteration that called it. That iteration will take those two dates and append its own EndDate, and so it continues back up the line until the original iteration is reached. Think of each iteration as drilling down into a new level until the bottom level is reached. At that point, the functions go back up the line, each iteration continuing until it is finished and returning back to its parent. For a concrete application, lets take an example of 1/1/2004 as the StartDate and 1/3/2004 as the EndDate. The first iteration will run with those two dates as the parameters. 1/3/2004 is indeed greater than 1/1/2004, so the case statement is met, and DateRange1() is called again. 1/1/2004 is passed to the second iteration as the StartDate, and 1/2/2004 is passed as the EndDate. In the second iteration, 1/2/2004 is indeed greater than 1/1/2004, so again the case statement is met, and a new iteration of DateRange1() is called. This time, it is passed 1/1/2004 as the StartDate and 1/1/2004 as the EndDate. In the third iteration, 1/1/2004 is not greater than 1/1/2004, so that iteration simply returns its EndDate: 1/1/2004 It returns to the second iteration, which appends a return and its own EndDate, 1/2/2004 and returns 1/1/2004 1/2/2004

Page 3 of 13

2004 Andrew Persons. All rights reserved.

to the first iteration, which appends its own EndDate, 1/3/2004, and returns that as the final value: 1/1/2004 1/2/2004 1/3/2004 Iteration Third Second First StartDate 1/1/2004 1/1/2004 1/1/2004 EndDate 1/1/2004 1/2/2004 1/3/2004 Value Returned 1/1/2004 1/1/2004 1/2/2004 1/1/2004 1/2/2004 1/3/2004

Ive sorted them in descending order because this is the way most recursive calculations will behave: start from the end, drill down to the beginning, and work your way back up.

Example 2: DateRange2()
Our DateRange1() function worked pretty well, but it relied on the user to be wellbehaved. What if the user entered an EndDate that was before the StartDate? Currently, the case statement in the first iteration would fail, and the function would simply return the EndDate. We should put in some error checking
DateRange2 ( StartDate ; EndDate ) = Case( StartDate > EndDate ; "Start date must be earlier than end date." ; Case ( EndDate > StartDate ; DateRange2 ( StartDate; EndDate - 1; RangeLimit) & "" & EndDate ; EndDate ))

Since we cant bring up an alert message with a calculation, well put our error message into the returned value instead. Now, if the EndDate isnt after the StartDate, the field will simply display this error message. Another possibility is that either the StartDate or EndDate are empty. If EndDate is empty, the case statement will fail, and the field will display nothing. If StartDate is empty, however, the case statement will not fail, and will enter an infinite loop, since the EndDate will forever be earlier than the StartDate. To prevent this from happening, well add another case statement:
DateRange2 ( StartDate ; EndDate ) = Case( IsEmpty ( StartDate ) or IsEmpty ( EndDate ); "Please fill in both fields."; StartDate > EndDate ; "Start date must be earlier than end
Page 4 of 13 2004 Andrew Persons. All rights reserved.

date." ; Case ( EndDate > StartDate ; DateRange2 ( StartDate; EndDate - 1; RangeLimit) & "" & EndDate ; EndDate ))

Theres just one more pitfall: Suppose the user entered 1/1/1000 and 1/1/2000? That would be 365,242 days, or 365,242 iterations! FileMaker would be busy for a very long time indeed. Wed better put in a date range limitation. Rather than putting in our own built-in limitation, lets give the user the control:
DateRange2 ( StartDate ; EndDate : RangeLimit) = Case( IsEmpty ( StartDate ) or IsEmpty ( EndDate ) ; "Please fill in both fields." ; StartDate > EndDate ; "Start date must be earlier than end date." ; ( EndDate - StartDate ) > ( RangeLimit * 365 ) ; "Date range has exceeded the limit of " & RangeLimit & " years."; Case ( EndDate > StartDate ; DateRange2 ( StartDate; EndDate - 1; RangeLimit) & "" & EndDate ; EndDate ))

Weve added a new parameter, RangeLimit. For convenience, well assume that its expressed in years to relieve the user of having to calculate the equivalent days, and multiply it by 365. This wont take leap years into account, but is intended to put a rough limit on the number of iterations that the function will go through. Otherwise, the user could easily lock FileMaker for extended periods of time in a lengthy recursion loop.

Example 3: Portal Filtering


This example will look familiar to many readers. Its designed to produce the key field necessary for portals that allow the user to type the first few letters of a word and have the portal display any records that match. This technique is also known as clairvoyance. The portal filter example includes four separate Custom Functions that give the user the tools to create the specific kind of key he wants. Lets look at the first one, FieldBeginsWith():
FieldBeginsWith ( Text ) = Case ( Length ( Text ) > 1 ; FieldBeginsWith ( Left ( Text ; Length ( Text ) - 1 ) ) & "" & Text; Text)

The first step is a case statement that sets up the conditions for the recursive loop to end (the exit condition):
Case ( Length ( Text ) > 1 ;

Page 5 of 13

2004 Andrew Persons. All rights reserved.

When the length of the Text parameter reaches one, the loop will end. If the case statement is true, the following is performed:
FieldBeginsWith ( Left ( Text ; Length ( Text ) - 1 ) ) & "" & Text

This calls the function again, and passes the same text, minus the rightmost character. This will continue until the text has been reduced to a length of one. At that point, the case statement will fail, and the second portion will be performed:
Text

This will return Text parameter to the previous iteration, which will append a carriage return and its own Text parameter, until the original iteration is reached. Heres the sequence of iterations if the text Hi World were passed to FieldBeginsWith(): Iteration Eighth Seventh Sixth Text H Hi Hi Value Returned H H Hi H Hi Hi H Hi Hi Hi W H Hi Hi Hi W Hi Wo H Hi Hi Hi W Hi Wo Hi Wor H Hi Hi Hi W Hi Wo Hi Wor
2004 Andrew Persons. All rights reserved.

Fifth

Hi W

Fourth

Hi Wo

Third

Hi Wor

Second

Hi Worl

Page 6 of 13

Hi Worl First Hi World H Hi Hi Hi W Hi Wo Hi Wor Hi Worl Hi World

Voil! You have the foreign key for a portal filter. By invoking one recursive function from within another, we can take this concept a step further in the next example well look at: WordBeginsWith():
WordBeginsWith ( Text ) = Case ( WordCount ( Text ) > 1 ; WordBeginsWith ( LeftWords ( Text ; WordCount ( Text ) - 1 ) ) & "" & FieldBeginsWith ( RightWords ( Text ; 1 ) ) & "" ; FieldBeginsWith ( RightWords ( Text ; 1 ) ) & "")

This function makes use of the FieldBeginsWith() function to parse out Text word by word. The exit condition here is when the number of words reaches one, rather that the number of letters. Similarly to FieldBeginsWith(), WordBeginsWith() keeps passing Text to new iterations of itself, but reducing the number of words by one each time. When Text has been reduced to one word, the second part of the case statement is performed:
FieldBeginsWith ( RightWords ( Text ; 1 ) ) & ""

FieldBeginsWith() is called, with the rightmost word as its parameter (since it has only one word, that will be the parameter). It then goes through all of its iterations, and returns its list of letters. The previous iteration of WordBeginsWith() then executes. This iteration will have two words, but only the rightmost one will be passed to FieldBeginsWith(), which will dutifully parse it out completely. This will continue until each word has been parsed out separately. This is an example of nested recursive calculations. WordBeginsWith() loops through all of its iterations, and each iteration calls FieldBeginsWith(), which loops through all of its iterations each time. Needless to say, this could quickly add up to a lot of iterations, so use with caution, and be sure to include limits on the size of the text to be parsed. Heres the chart of the iterations for Hi World:

Page 7 of 13

2004 Andrew Persons. All rights reserved.

Iteration Second First

Text Hi Hi World

Value Returned H Hi H Hi W Wo Wor Worl World

Example 4: FormatPhone()
This final example is a bit more complex than the others. This function takes a phone number, a format string such as ###-###-####, and a wildcard string which tells the function which character represents a digit (in this case #). Use it in the auto-enter calculation for your phone number field, and uncheck the Do not replace existing value of field (if any) checkbox. It will automatically format any number you put in the field. This also allows you to give the user the ability to set what format they want in a Preferences layout. It makes use of two functions, FormatPhone() and FormatLoop(). The second function is recursive and nested within the first, which is called only once. Lets start with FormatLoop():
FormatLoop ( PhoneNum ; Format ; WildCard ) = Let ( [ PhoneAsNum = Filter ( PhoneNum ; "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv wxyz"); Form = If ( Length ( PhoneAsNum ) > 1 ; FormatLoop ( Left ( PhoneAsNum ; Length ( PhoneAsNum ) - 1 ) ; Format ; Wildcard) ; Format ) ; Pos = Position ( Form ; Wildcard ; 1 ; 1 ) ; Digit = Let ( Char = Right ( PhoneAsNum ; 1 ) ; Case ( GetAsNumber ( Filter ( Char Filter ( Char Filter ( Char Char ) = Char ; Char ; ; "ABCabc" ) = Char ; 2 ; ; "DEFdef" ) = Char ; 3 ; ; "GHIghi" ) = Char ; 4 ;

Page 8 of 13

2004 Andrew Persons. All rights reserved.

Filter Filter Filter Filter Filter ] ;

( ( ( ( (

Char Char Char Char Char

; ; ; ; ;

"JKLjkl" ) "MNOmno" ) "PQRSpqrs" "TUVtuv" ) "WXYZwxyz"

= = ) = )

Char ; Char ; = Char Char ; = Char

5 6 ; 8 ;

; ; 7 ; ; 9 ))

If ( Pos = 0 ; Form & Digit ; Replace ( Form ; Pos ; 1 ; Digit ) ) )

Let() definitions make up the majority of the function. Let() is a FileMaker Pro 7 feature that allows you to define variables in calculations. It makes our job easier for this Custom Function by making the logic more readable; it also improves performance by only calculating a value once for each iteration. For more information on the Let() function, see the FileMaker Pro 7 documentation. The first variable defined is PhoneAsNum. This uses the Filter() function to remove all non-alphanumeric characters from the phone number. Any additional characters that you wish to be included can easily be added to the filter.
PhoneAsNum = Filter ( PhoneNum ; "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv wxyz");

The second variable is Form. This is the meat of the function, and where the actual recursion takes place.
Form = If ( Length ( PhoneAsNum ) > 1 ; FormatLoop ( Left ( PhoneAsNum ; Length ( PhoneAsNum ) - 1 ) ; Format ; Wildcard) ; Format ) ;

This follows the pattern weve seen in our previous examples: exit condition, decrementing parameters, exit statement. Exit condition: If ( Length ( PhoneAsNum ) > 1 Decrementing parameter(s): FormatLoop ( Left ( PhoneAsNum ; Length (
PhoneAsNum ) - 1 ) ; Format ; Wildcard)

Exit statement: Format This calls itself with the same Format and Wildcard parameters that it received, but passes the stripped PhoneAsNum variable, minus one digit. When it reaches one digit, the calculation will continue. The third variable is Pos, which is the position in the Form variable of the first wildcard character (# in this case).
Pos = Position ( Form ; Wildcard ; 1 ; 1 ) ;

The fourth variable is Digit. This replaces any letters in the phone number with the corresponding digits from the telephone keypad.
Page 9 of 13 2004 Andrew Persons. All rights reserved.

Digit = Let ( Char = Right ( PhoneAsNum ; 1 ) ; Case ( GetAsNumber ( Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Char ) = Char ; Char ; ; "ABCabc" ) = Char ; 2 ; "DEFdef" ) = Char ; 3 ; "GHIghi" ) = Char ; 4 ; "JKLjkl" ) = Char ; 5 ; "MNOmno" ) = Char ; 6 ; "PQRSpqrs" ) = Char ; ; "TUVtuv" ) = Char ; 8 ; "WXYZwxyz" ) = Char ; ; ; ; ; ; 7 ; ; 9 ))

After the variables have been declared, the final part of the function simply puts the pieces together:
If ( Pos = 0 ; Form & Digit ; Replace ( Form ; Pos ; 1 ; Digit ) ) )

Before we analyze this final section, lets review what happens during the variable definition of Form, with the following input: PhoneNum = 8005551234 Format = ###-###-#### Wildcard = # FormatLoop() is called, with the same Format and Wildcard, but PhoneAsNum is reduced by one digit each time. When the length of PhoneAsNum reaches one, Format is returned (so Form = ###-###-####) and the function continues.
Pos = Position ( Form ; Wildcard ; 1 ; 1 )

So Pos = 1, since the first occurrence of # is the first digit. Lets review the portion of the function that were talking about:
Digit = Let ( Char = Right ( PhoneAsNum ; 1 ) ; Case ( GetAsNumber ( Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Char ) = Char ; Char ; ; "ABCabc" ) = Char ; 2 ; "DEFdef" ) = Char ; 3 ; "GHIghi" ) = Char ; 4 ; "JKLjkl" ) = Char ; 5 ; "MNOmno" ) = Char ; 6 ; "PQRSpqrs" ) = Char ; ; "TUVtuv" ) = Char ; 8 ; "WXYZwxyz" ) = Char ; ; ; ; ; ; 7 ; ; 9 ))

Page 10 of 13

2004 Andrew Persons. All rights reserved.

The rightmost character in PhoneAsNum at this point is 8 (because PhoneAsNum has been reduced to only one character), so digit = 8.
If ( Pos = 0 ; Form & Digit ; Replace ( Form ; Pos ; 1 ; Digit ) )

Position 0, so the Replace is performed. Heres the Replace() with the variables replaced with their values.
Replace ( ###-###-#### ; 1 ; 1 ; 8 )

It will return this: 8##-###-####. The function then returns to the previous iteration. Now, in this iteration, Form = 8##-###-####. Continuing in the function:
Pos = Position ( Form ; Wildcard ; 1 ; 1 ) ;

Now, Pos = 2. Heres what happens next:


Digit = Let ( Char = Right ( PhoneAsNum ; 1 ) ; Case ( GetAsNumber ( Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Filter ( Char Char ) = Char ; Char ; ; "ABCabc" ) = Char ; 2 ; "DEFdef" ) = Char ; 3 ; "GHIghi" ) = Char ; 4 ; "JKLjkl" ) = Char ; 5 ; "MNOmno" ) = Char ; 6 ; "PQRSpqrs" ) = Char ; ; "TUVtuv" ) = Char ; 8 ; "WXYZwxyz" ) = Char ; ; ; ; ; ; 7 ; ; 9 ))

In this iteration, PhoneAsNum = 80, so Digit = 0.


If ( Pos = 0 ; Form & Digit ; Replace ( Form ; Pos ; 1 ; Digit ) ) )

Again, Pos 0, so heres the replace with the actual values:


Replace ( 8##-###-#### ; 2 ; 1 ; 0 )

This will return 80#-###-####. This will continue back up through the iterations, each time replacing the next occurrence of # with the next digit. When there are no more instances of # (i.e., Pos = 0), it will start appending Digit to the end of Form. Heres the chart of iterations: Iteration PhoneNum Value Returned to Form

Page 11 of 13

2004 Andrew Persons. All rights reserved.

Tenth Ninth Eighth Seventh Sixth Fifth Fourth Third Second First

8 80 800 8005 80055 800555 8005551 80055512 800555123 8005551234

8##-###-#### 80#-###-#### 800-###-#### 800-5##-#### 800-55#-#### 800-555-#### 800-555-1### 800-555-12## 800-555-123# 800-555-1234

This function works well, except for one thing: what if the number of digits in PhoneNum is less than the number of wildcards in Format? Heres what the result would look like if 800555 and ###-###-#### were the PhoneNum and Format, respectively. 800-555-#### Obviously, this isnt desirable. An easy way to remove the extra wildcard characters would be to use Substitute() at the very end. Unfortunately, each iteration doesnt know if its the last one or not, unless we added an additional parameter. Instead of doing that, lets just create another function that will call FormatLoop(), and clean up after it.
Case ( not IsEmpty ( Filter ( PhoneNumber ; "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuv wxyz") ) ; Case ( not IsEmpty(PhoneFormat) ; Substitute ( FormatLoop ( PhoneNumber ; PhoneFormat ; WildCard) ; WildCard ; "" ) ; PhoneNumber ) )

This function simply passes the parameters to FormatLoop(), and uses Substitute() to clean up. It checks that PhoneNumber isnt empty, so that there arent extraneous characters in the field; it also checks that PhoneFormat isnt empty, and simply returns PhoneNumber if it is.

Page 12 of 13

2004 Andrew Persons. All rights reserved.

In Summary
Recursive calculations are an enormously useful addition to FileMaker Pros repertoire. They may look intimidating, but are well worth the effort to master. Just remember this general approach 1. Start at the end 2. Drill down to the beginning 3. Work your way back up ...and include the following elements... 1. Exit Condition (what will end the loop) 2. Decrementing (or Incrementing) Parameters (what you pass to each new iteration) 3. Exit Statement (what the final iteration will perform) Things to keep in mind about recursive calculations: They can only be created using Custom Functions. Custom Functions can only be designed with FileMaker Developer 7, but the regular version of FileMaker Pro 7 can use them once theyve been created. A recursive calculation must provide a mechanism to end the loop, or it will run for 10,000 iterations, or when FileMaker runs out of memory, whichever comes first. If there is any possibility that the recursive calculation could end up producing a large number of iterations, consider putting in conditions that will limit the number of times it will run. Recursion is powerful, but it can also get you in trouble if youre not careful.

Page 13 of 13

2004 Andrew Persons. All rights reserved.

Вам также может понравиться