Jump to content

While Loop in a Custom Function?


This topic is 4393 days old. Please don't post here. Open a new topic instead.

Recommended Posts

I'm trying to adapt a formula written in javascript into a Filemaker pro custom function. I have it functioning as a script, but I don't see a way to write this as a function since it needs to loop. Any help is appreciated.

The original formula is found near the bottom of this page: http://www.movable-t...g-vincenty.html

And I've attached an print of the scripted calculation (the system won't let me upload an example file).

The latitude/longitude of 2 different points are passed to the script, and the result is the distance between them.

formula.pdf

Link to comment
Share on other sites

I havent analysed your problem very deeply.

But I think that if you split your problem into two custom functions.

The first CS takes all the input and defining all the $cos ao. variables as global '$$cos', and in the middle of that CS call the second CS with the lambda and iterLimit parameters (and others i might have missed that changes)

The second CS then iterates passing theese variables, and on exit you could have it setting some global variables, that will be used to decide what to do on the return to the first CS.

Link to comment
Share on other sites

Hello Lucky, and welcome to the forum,

In the future please just attach your file to a reply using the More Reply Options button.

If you have any questions about this request, please send me a private mail.

Lee

Link to comment
Share on other sites

Here's an example of a custom function implementing a loop as [tail] recursion without the need for a second "interface" or "helper" function or any function arguments: https://github.com/filemakerstandards/fmpstandards/blob/master/Functions/UUID/UUIDTimeNIC.fmfn

The "Parse NIC address from hexadecimal to a (base 10) number" section contains the looped code. This example is a for loop rather than a while loop, but I think anyone who knows the difference is probably bright enough to figure out how to get what they want.

Link to comment
Share on other sites

Well, this is my first attempt at writing a recursive function and I think I bit off more than I can chew. Here's what I have, although it's not working. The red text is where I'm stumped. The variable needs to test for exiting the loop, but also exiting the function without further processing.

----

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

/* Vincenty Inverse Solution of Geodesics on the Ellipsoid © Chris Veness 2002-2011 */

/* Modified for Filemaker Pro by Chuck Humbertson 2012 */

/* from: Vincenty inverse formula - T Vincenty, "Direct and Inverse Solutions of Geodesics on the */

/* Ellipsoid with application of nested equations", Survey Review, vol XXII no 176, 1975 */

/* http://www.ngs.noaa....LIB/inverse.pdf */

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

/*

* Calculates geodetic distance between two points specified by latitude/longitude using

* Vincenty inverse formula for ellipsoids

*

* @param {Number} lat1, lon1: first point in decimal degrees

* @param {Number} lat2, lon2: second point in decimal degrees

* @returns {Number} distance in metres between points

* $$step controls Case() branching

*/

Case (

/* First stage */

$$step = 0 ;

Let ( [

$a = 6378137 ;

$b = 6356752.314245 ;

$f = 1 / 298.257223563 ; // WGS-84 ellipsoid params

$L = ( lon2 - lon1 ) * Pi / 180 ;

$U1 = Atan ( ( 1 - $f ) * Tan ( lat1 * Pi / 180 ) ) ;

$U2 = Atan ( ( 1 - $f ) * Tan ( lat2 * Pi / 180 ) ) ;

$sinU1 = Sin ( $U1 ) ;

$cosU1 = Cos ( $U1 ) ;

$sinU2 = Sin ( $U2 ) ;

$cosU2 = Cos ( $U2 ) ;

$lambda = $L ;

$iterLimit = 100

] ;

$$step = 1 // set variable to go to 2nd stage of loop

) ;

/* Second stage (Begin Main Loop) */

$$step = 1 ;

Let ( [

$sinLambda = Sin ( $lambda ) ;

$cosLambda = Cos ( $lambda ) ;

$sinSigma = Sqrt ( ( $cosU2 * $sinLambda ) * ( $cosU2 * $sinLambda ) + ( $cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda ) * ( $cosU1 * $sinU2 - $sinU1 * $cosU2 * $cosLambda ) )

] ;

$$step =

If ( $sinSigma = 0 ; 5 ; /*else*/ $$step + 1 ) // if co-incident points -- $step=5 = exit function

) ;

/* Third stage */

$$step = 2 ;

Let ( [

$cosSigma = $sinU1 * $sinU2 + $cosU1 * $cosU2 * $cosLambda ;

$sigma = fnAtan2 ( $sinSigma ; $cosSigma ) ;

$sinAlpha = $cosU1 * $cosU2 * $sinLambda / $sinSigma ;

$cosSqAlpha = 1 - $sinAlpha * $sinAlpha ;

$cos2SigmaM = $cosSigma - 2 * $sinU1 * $sinU2 / $cosSqAlpha ;

$cos2SigmaM =

If ( IsValid ( $cos2SigmaM ) ; $cos2SigmaM ; 0 ) ; // equatorial line: cosSqAlpha=0 (§6)

$C = $f / 16 * $cosSqAlpha * ( 4 + $f * ( 4 - 3 * $cosSqAlpha ) ) ;

$lambdaP = $lambda ;

$lambda = $L + ( 1 - $C ) * $f * $sinAlpha * ( $sigma + $C * $sinSigma * ( $cos2SigmaM + $C * $cosSigma * ( -1 + 2 * $cos2SigmaM * $cos2SigmaM ) ) ) ;

$iterLimit = $iterLimit - 1 ;

$$step =

If ( $$step ≠ 5 and Abs ( $lambda - $lambdaP ) > 1.0e-12 ; 1 ; 3 ) ;

$$step =

If ( $iterLimit = 0 ; 5 ; 1 )

] ;

fnVincenty ( lat1 ; lon1 ; lat2 ; lon2 )

) ; // End Main loop

/* Fourth stage */

$$step = 3 ;

Let ( [

$uSq = $cosSqAlpha * ( $a * $a - $b * $b ) / ( $b * $b ) ;

$A = 1 + $uSq / 16384 * ( 4096 + $uSq * ( -768 + $uSq * ( 320 - 175 * $uSq ) ) ) ;

$B = $uSq / 1024 * ( 256 + $uSq * ( -128 + $uSq * ( 74 - 47 * $uSq ) ) ) ;

$deltaSigma = $B * $sinSigma * ( $cos2SigmaM + $B / 4 * ( $cosSigma * ( -1 + 2 * $cos2SigmaM * $cos2SigmaM ) - $B / 6 * $cos2SigmaM * ( -3 + 4 * $sinSigma * $sinSigma ) * ( -3 + 4 * $cos2SigmaM * $cos2SigmaM ) ) )

] ;

$$dist = $b * $A * ( $sigma - $deltaSigma )

) ;

/* End stage */

$$step = 5

// Terminate processing because either 1) the points are co-incident,

// or 2) the formula failed to converge.

)

Link to comment
Share on other sites

Using $var and $$var is not good practice. The variables inside the cals can interact in unexpected ways with variables of the same name OuTSIDE the calc.

Does this need to be recursive?

I did something to calculate distances between points some time ago and did not need recursion.

http://www.briandunning.com/cf/143

Link to comment
Share on other sites

Does this need to be recursive?

Yes it does. That is, if you really need that kind of accuracy - otherwise you could use one of the non-recursive algorithms, such as GCD or Haversine.

I think I bit off more than I can chew.

That is possible. I have looked at the script you have posted earlier, and it seems to return results that are different from the expected.

Link to comment
Share on other sites

I wouldn't say never use variables in custom functions, but it needs to be approached very carefully. I only use variables when I need to refer to the same data between separate calls to the same function; values that only need to be used for one function call are better as extra-local Let() variables without any "$" prefix. When I do use $variables, I like to prefix them with something that will reduce the chance of me using the same variable name for a different purpose in a conflicting way, like $~uuid.step.

I notice that you're using a global $$step variable rather than a local $step — to share data between recursive calls to the same function, you only need local variables. I know it doesn't seem like that would work, but it does. I only use global $$variables for data that needs to be saved throughout a session. That UUID function has to store an incrementing serial number to work right, and it stores the base-10 NIC address as a performance improvement so the function doesn't have to recalculate that value every time.

I also make sure to erase the value I set for any local variables in the last step so successive calls to the same function within the same calculation or script don't "pollute" each other. This means that I can't end evaluation completely without evaluating the last stage. I can short-circuit the execution for error conditions by setting $step = -1, then the condition for executing the last step is something like "$step = NN or $step = -1", so, even when I skip most of the calculation due to an error, the variables still get wiped.

In the Let() function for each stage of the function, the portion where you set the $$step (preferably $step!) variable should be in the variable-declaration portion (between the brakets). The expression portion of the Let() function should contain the recursive call to your function.

Case (

...

$~func.step = 1;

Let ( [

...

$~func.step = If ( condition ; $~func.step ; /* Else */ $~func.step + 1 )

];

Function ( argument )

);

...

$~func.step = 10 or $~func.step = -1; // last step, or error

Let ( [

~result = $~func.result;

~error = $~func.step = -1; // boolean result of testing if there was an error

// purge variables

$~func.result = "";

$~func.step = ""

];

If ( ~error ; "?" ; /* Else */ ~result )

)

Link to comment
Share on other sites

There is no reason to use $variables in a cf. A "let" variable does everything a $var does. Using the $var adds a risk of problems.

There MAY be a reason to use a global $$var because it has functionality that nothing else can replace (other than another parameter in the function) and I have done so myself. The var name was suitably chosen to minimise risk of unexpected interaction.

Link to comment
Share on other sites

Vaughan, I think Jeremy's point was that a '$var' variables value will persist throughout all recursions of a custom function, so they can be used to prevent the custom function from needing an additional parameter, or to increase performance by storing a value that would otherwise need constant re-calculation. Then, any value that only needs to exist for a single recursion should be a local 'let' variable (no $).

Link to comment
Share on other sites

Using $var and $$var is not good practice. The variables inside the cals can interact in unexpected ways with variables of the same name OuTSIDE the calc.

Does this need to be recursive?

I did something to calculate distances between points some time ago and did not need recursion.

http://www.briandunning.com/cf/143

Well I wasn't sure if Let() variables would persist through the several recursions needed to arrive at an answer. I am aware of the CF you wrote which uses the cosine-haversine method. However this Vincenty method gives an accuracy down to the nearest millimeter.

I first tried to do this without using any $ variables but in the example I posted above, when I clicked okay the FM threw up an error message saying it could not find the "lambda" used in the 2nd stage, which gets calculated in the first stage (recursion). That's why I went hog wild with the "$"s :logik: .

My thinking now is that I will need an additional variable for control purposes, one to control Case() branching and another to control early termination of the function. I don't see how it can be done with just one in the second red text part of the code above.

Link to comment
Share on other sites

I wouldn't say never use variables in custom functions, but it needs to be approached very carefully. I only use variables when I need to refer to the same data between separate calls to the same function; values that only need to be used for one function call are better as extra-local Let() variables without any "$" prefix. When I do use $variables, I like to prefix them with something that will reduce the chance of me using the same variable name for a different purpose in a conflicting way, like $~uuid.step.

I notice that you're using a global $$step variable rather than a local $step — to share data between recursive calls to the same function, you only need local variables. I know it doesn't seem like that would work, but it does. I only use global $$variables for data that needs to be saved throughout a session. That UUID function has to store an incrementing serial number to work right, and it stores the base-10 NIC address as a performance improvement so the function doesn't have to recalculate that value every time.

I also make sure to erase the value I set for any local variables in the last step so successive calls to the same function within the same calculation or script don't "pollute" each other. This means that I can't end evaluation completely without evaluating the last stage. I can short-circuit the execution for error conditions by setting $step = -1, then the condition for executing the last step is something like "$step = NN or $step = -1", so, even when I skip most of the calculation due to an error, the variables still get wiped.

In the Let() function for each stage of the function, the portion where you set the $$step (preferably $step!) variable should be in the variable-declaration portion (between the brakets). The expression portion of the Let() function should contain the recursive call to your function.

Thanks, I'm going to take this all in and see how I can rewrite it.

Vaughan, I think Jeremy's point was that a '$var' variables value will persist throughout all recursions of a custom function, so they can be used to prevent the custom function from needing an additional parameter, or to increase performance by storing a value that would otherwise need constant re-calculation. Then, any value that only needs to exist for a single recursion should be a local 'let' variable (no $).

That's what I wasn't clear about beforehand. Will try again.

Link to comment
Share on other sites

Yes it does. That is, if you really need that kind of accuracy - otherwise you could use one of the non-recursive algorithms, such as GCD or Haversine.

That is possible. I have looked at the script you have posted earlier, and it seems to return results that are different from the expected.

I'll check it out. I didn't test it for all possible values; just wanted to adapt it to FMPro.

Okay, I updated the example file (same link as above). (I had forgotten to include a helper CF in that file).

Also @Lee, I tried just now to attach the file via the method you described but I'm still getting an error message "You aren't permitted to upload this kind of file."

Link to comment
Share on other sites

What started me on all this was that I wanted to be able to write this as a calculated field definition. I have the Contact Management solution that shipped with FMPro. On the Related Contacts tab you can match related contacts through a self-join relationship. I wanted to calculate the distance between two contacts (the one displayed as the current record being the parent table) and show that in the portal with the related records. (I already wrote a script that scrapes the lat/lon from Google whenever a new record is entered).

This requires a calculated field, and as far as I'm aware I can't use scripting techniques in a calculated field definition -- it's almost the same as writing a CF. However, I can call a CF from a calc. field definition.

So that's how I got where I am. I will see if I can write 2 CFs and call one from the other.

Another reason is because I wanted to see if I could pull this off, looking at it as a challenge.

Link to comment
Share on other sites

As a challenge it's good, though perhaps not the best to be starting with. In terms of your stated purpose, I am not sure the added "accuracy" is worth the effort - both yours and the CPU's.

I put "accuracy" in quotes, because I suspect local terrain conditions may cause variations greater than the difference between the formulae.

I will see if I can write 2 CFs and call one from the other.

Technically, you only need the sub-function; the rest of the calculation - both before and after the loop - can be done inside the calculation field formula itself.

Edited by comment
Link to comment
Share on other sites

This topic is 4393 days old. Please don't post here. Open a new topic instead.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.