This article is part of my series Custom Function Vault. Here I describe miscellaneous custom functions to verify required script parameters.
param.script.check()text.between( text; start; end; type )trim.all( text; trimChr )var.assign( definition )
Continue the Project
Last time I wrote about the custom function param.assign. It will take a list of named parameters and creates local variables with the same names and values. This is the first step for processing and checking required script parameters. Now I will describe the second part to finish this project.
If you want to verify whether all parameters for a specific script are defined, then you need to know first, which parameters are required for the script. A convenient way to publish the parameter names is to include them in the script name.
Formal Script Names
Very early on I used to name my scripts with the name of the script parameters included:
Select record( id; [ layout ] )
Go to layout( { name | number }; [ mode ] )
Doing it this way will help to understand better what the script is doing and, of course, what values have to be passed to the script. Also, the script name looks like a function definition, something we are familiar from FileMaker’s function editor.
For the parameter list I use a specific syntax:
- The parameter list follows the script name, enclosed in round brackets:
Script name( parameter list ) - Parameter names are written in camel-case format without spaces (one word equals one parameter):
bookingCode - Mandatory parameters are entered with their plain names:
id - Optional parameters are written in square brackets:
[ layout ] - Alternative parameters are entered in curly brackets, separated by a vertical bar:
{ name | number }
At least one parameter from this group has to be not empty. - Parameters or parameter groups are are separated by a semicolon:
{ name | number }; [ mode ] - Spaces around parameter names are ignored:
{ name | number }or{name|number}
Parsing Parameter Names
I like Alexander Zuiev’s idea (I have heard of at Matt Petrowsky’s website) to parse a script name and check for required parameters. My syntax should do well with this idea. All parameter names are part of the script name and their type (mandatory, optional, alternativ) are clearly marked.
To check for required parameters, the parameter list can be transormed into a logical expression:
- Each parameter group correlates a logical term. All terms are linked with logical ANDs.
- An optional parameter is transformed into an expression that will always return TRUE:
(TRUE or parameter) - A group of alternative parameters is transformed into an expression like this:
(paramter1 OR parameter2 OR … ) - Finally, every parameter is replaced with the result of the expression:
not IsEmpty( parameter )
Evaluating this expression will return either TRUE or FALSE, indication whether all required parameters are available.
CF var.check( definition )
The custom function expects a parameter definition (text) following my syntax for parameter in script names. It will transform the definition into a logical expression and then replace every parameter name, one at a time.
Case(
not IsEmpty( $var.check_vars );
Let( [
_var = Trim( GetValue( $var.check_vars; 1 ) );
$var.check_vars = MiddleValues( $var.check_vars; 2; 1000000 );
$var.check_code = Substitute( $var.check_code; " " & _var & " "; " " & Evaluate( "not IsEmpty( $" & _var & " )" ) & " " );
_temp = var.check( "" )
];
""
);
IsEmpty( definition );
"";
// Else
Let( [
_def = Substitute( definition; ";"; ¶ );
$var.check_vars = Substitute( _def; ["{"; ""]; ["}"; ""]; ["["; ""]; ["]"; ""]; ["|"; ¶] );
$var.check_code = " " & Substitute( _def;
// Optional parameter
["["; "(TRUE or "]; ["]"; ")"];
// Alternative parameter
["{"; "("]; ["|"; " or "]; ["}"; " )"];
// Connecting terms
[¶; " and "]
) & " ";
_temp = var.check( "" )
];
Evaluate( $var.check_code )
)
)
What is it doing?
This custom function need some explanation. First of all, this function is a recursive function, that means, it calls itself. The first call will initiate the function and setup the definition string. Then it will recall itself for every parameter defined.
Second, I use local variables to store data and keep them available between recursion loops. Usually I name these variables after the function they are used in, plus an underscore and a data name, e. g. $var.check_vars.
The custom function var.assign starts with a case function, differentiating between three cases. The order of these case is actual important:
- The variable
$var.check_varsis not empty. - The function parameter definition is empty.
- All other cases.
When the function is called the first time, the cases 1 and 2 will not apply. Therefore, the Else part is executed. It creates two local variables:
$var.check_vars– A list with all defined parameters is assigned to it.$var.check_code– It will receive the parameter definition, transformed into a logical expression (as explained above).
After the setup of these variables, the function calls itself. After this and all following recursion, the result in $var.check_code is returned as the function result.
1st and consecutive Recursion
When the function is called the second time and again, the first case (variable is not empty) applies. In this part of the case function, the top entry from the list with all defined parameter is removed, tested for fullness, and the result replaced in the variable $var.check_code. Then, the function calls itself again.
The first case will be repeated until the variable with the parameter list is empty. This will activate the second part of the case function. Not much is happening here, but it is rather important, because it stops all the recursions. Now that no more recursion loop is active, the last line of the third case function is executed, hence the variable with the logical expression evaluated and returned as the function result.
Example
var.check( "recId; { layoutName | layoutNo }; [mode]" )
returns TRUE (1), when the local variable $recId = 4712 and $layoutNo = 8.
CF text.between( text; start; end; type )
Now that we can check for all required parameters, we need to retrieve the parameter list from the script name. I could do it by searching for the first round bracket in the script name and return everthing behind it, but I have already a custom function that does this.
Let( [
_length = Length( text );
_startPos = If( IsEmpty( start ); 1; Position( text; start; 1; 1 ) );
_startPos = If( _startPos = 0; 0; _startPos + Length( start ) );
_endPos = Case(
IsEmpty( end );
_length + 1;
type = "last" or type = "long";
Position( text; end; _length; -1 );
// else (next | short | or anything else )
Position( text; end; _startPos; 1 )
)
];
If( _startPos = 0 or _endPos = 0;
"";
// Else
Middle( text; _startPos; _endPos - _startPos )
)
)
The function text.between expects four parameter:
- text – The text to be processed, in our case the script name.
- start – The start text we are looking for, in our case the left round bracket.
- end – The end text we are looking for, in our case the right round bracket.
- type – The way the end text is searched: “next” or “short” – the first appearance of end text after start text; “last” or “long” – the last possible end text.
Example
text.between( "1 + ( 2 + ( 3 + 4 ) ) )"; "("; ")"; "last" )
returns ” 2 + ( 3 + 4 ) ) “.
CF trim.all( text; trimChr )
Another usefull custom function is trim.all. The trim function in FileMaker removes only space characters. My function will remove any character defined in trimChr from the start and end of text.
Let( [
trimChr = If( IsEmpty( trimChr ); " ¶"; trimChr ); // Default whitespace
_length = Length( text );
_trimLeft = GetAsBoolean( Position( trimChr; Left( text; 1 ); 1; 1 ) > 0 );
_trimRight = GetAsBoolean( Position( trimChr; Right( text; 1 ); 1; 1 ) > 0 );
text = Middle( text; 1 + _trimLeft; _length - _trimLeft - _trimRight )
];
If( _trimLeft or _trimRight;
trim.all( text; trimChr );
// Else
text
)
)
Noteworthy
If trimChr is left empty, a default setting (whitespace: space, tab, carriage return) is used. When you copy this function you might have to change the character before the carriage return (¶) manually into a tab character after you paste it into FileMaker.
What is it doing?
This function calls itself. In each loop it tests whether the first or last character of text belongs to trimChr. The text is then shortend accordingly. This will be repeated until the first and last character are not found in trimChr.
CF param.script.check( )
Now we can put everything together to check the required script parameter.
Let( [
_count = param.assign( Get( ScriptParameter ) );
_paramDef = Substitute( text.between( Get( ScriptName ); "("; ")"; "last" ); ";"; ¶ );
_paramName = trim.all( GetValue( _paramDef; 1 ); _ );
_temp = If( _count = 0; Evaluate( "Let($" & _paramName & "=" & Quote( Get( ScriptParameter ) ) & "; \"\" )" ) )
];
var.check( _paramDef )
)
What is it doing?
First, it assigns all parameter pass to the script to local variables. The function param.assign was described in a previous article. The next line retrieves the parameter definition from the script name.
The next two lines take care of the lazy developer.
Usually, all parameter should be passed with the function param to the script, as described in the article Manage Parameters. But sometimes, when there is exactly one parameter required for the script (despite any optional parameter, but the required parameter has to be the first in line), the parameter can be passed not decoded. It will be assigned to the first defined parameter.
After the whole setup, the definition list is checked and the result returned.
Using it in Scripts
Just include the following construct into your script and you are good to go:
If [param.script.check] ...Script commands, using all script parameters right away... Else ...Not all required parameters are set... End If
The whole setup is now pretty simple. With just one custom function all script parameters are assigned to local variables and checked against a list of required parameters. I am using this in all my projects now.