Custom "Robust" eval()

Discuss macro implementations, ask for macro help (to share your creations, see User Creations, probably either Campaign Frameworks or Drop-in Resources).

Moderators: dorpond, trevor, Azhrei, giliath, jay, Mr.Ice

Post Reply
User avatar
zEal
Dragon
Posts: 944
Joined: Sun Mar 22, 2009 2:25 am

Custom "Robust" eval()

Post by zEal »

A redefinition of the eval() function to work around its limitations.

Features this custom version has that the standard eval() lacks:
  • Returns numbers instead of providing an error.
  • Returns empty strings instead of providing an error.
  • Returns JSON objects/arrays instead of providing an error.
Place the following two macros on a library token:

1.3b56+

onCampaignLoad

Code: Select all

[ defineFunction( "eval", "evalFunction@this", 1, 0 ) ] 
evalFunction

Code: Select all

//  Error handling
[ assert( argCount() >= 1, "<b>eval()</b> - Invalid number of parameters <i>0</i>,
                            expected at least <i>1</i> parameter.", 0 ) ]

//  Initialise variables
[ X_Expression_X = arg( argCount()-1 ) ]
[ X_CancelEval_X = 0 ]
[ X_TypeTest_X = json.type( X_Expression_X ) ]

//  Handle all numbers
[ if( isNumber( X_Expression_X ) == 1 ), code:
{
   [ X_CancelEval_X = 1 ]
} ]

//  Handle empty strings
[ if( X_TypeTest_X == "UNKNOWN" ), code:
{
    [ if( X_Expression_X == "" ), code:
    {
        [ X_CancelEval_X = 1 ]
    } ]
} ]

//  Handle JSON types
[ if( X_TypeTest_X == "ARRAY" || X_TypeTest_X == "OBJECT" ), code:
{
    [ X_CancelEval_X = 1 ]
} ]

//  Evaluate or cancel, then return
[ if( X_CancelEval_X == 1 ), code:
{
    [ macro.return = X_Expression_X ]
};{
    [ macro.return = oldFunction( X_Expression_X ) ]
} ] 

Original Post
I see this problem crop up from time to time, and I've run into it myself; eval() chokes when fed a zero. To combat this I used to use a user defined function called myEval(), but I was recently(today) exposed to a function that would allow it to be far more elegant. So, I present to you, a custom eval().

Place the following in an onCampaignLoad macro, modifying the library token to suit your needs.

Code: Select all

[defineFunction("eval", "evalFunction@Lib:Utilities")]
On the library token referenced in the defineFunction() call, create a macro named evalFunction, and paste the following into the Command:

Code: Select all

[h: assert( argCount() == 1, "eval() requires one parameter.")]
[h, if ( arg(0) == 0 ), code:
{
    [h: macro.return = 0]
};{
    [h: macro.return = oldFunction( arg(0) ) ]
}]
You can now continue to use eval() as you normally would, and never again worry about feeding it a zero.
Last edited by zEal on Wed Jun 17, 2009 5:21 pm, edited 4 times in total.

User avatar
lmarkus001
Great Wyrm
Posts: 1867
Joined: Sat Mar 29, 2008 12:30 am
Location: Layfayette Hill, PA

Re: Custom eval() - "Zero Proof"

Post by lmarkus001 »

Building on your good idea, I also run in to cases where I try to feed eval() a null string "". So here is an updated evalFunction macro

evalFunction

Code: Select all

<!--
[H: assert( argCount() == 1, "eval() requires one parameter.")]
[H, IF( arg(0) == 0 ): macro.return = 0]
[H, IF( arg(0) == "" ): macro.return = ""]
[H, IF( arg(0) != 0 && arg(0) != ""): macro.return = oldFunction( arg(0) )]
-->

User avatar
zEal
Dragon
Posts: 944
Joined: Sun Mar 22, 2009 2:25 am

Re: Custom eval() - "Zero Proof"

Post by zEal »

There were actually a number of flaws in the original implementation:

Didn't handle empty strings.
Didn't handle JSON objects/arrays like the original.. nor better.
Didn't handle other numbers besides zero.
Didn't handle multiple parameters.

The standard eval() would puke if given any number or an empty string, would cancel macro execution and provide an error message if given a JSON object/array, and would accept multiple parameters but only evaluate the last.

I've been using a more robust version for a while now, I've just neglected to upload it. >.> It returns the parameter unaltered if given a number, empty string, or JSON object/array, and handles multiple parameters in the same manner as the standard eval(). I've gone ahead and updated the original post to include this more robust version. :)

User avatar
lmarkus001
Great Wyrm
Posts: 1867
Joined: Sat Mar 29, 2008 12:30 am
Location: Layfayette Hill, PA

Re: Custom "Robust" eval()

Post by lmarkus001 »

Wait! The SLASH SLASH comment delimiter went live in b56? Woo hoo!

User avatar
lmarkus001
Great Wyrm
Posts: 1867
Joined: Sat Mar 29, 2008 12:30 am
Location: Layfayette Hill, PA

Re: Custom "Robust" eval()

Post by lmarkus001 »

Hummm... this function is failing for calls that are structured like (assemble a variable name from two text strings):

Code: Select all

[H: jST = "[]"]
[H, FOREACH(st, rSubtypeList), CODE: {
  [R: temp = eval(st + "Subtype")]
  [IF(temp): jST = json.append(jST, st)]
}]
Where I have a bunch of variables defined for an input dialog that look like AirSubtype, AngelSubtype, AquaticSubtype, etc. So I want the eval() to find the value of the variable that is formed from assembling the two text strings.

If I feed this code to the real eval() it works fine, if fed to the improved evalFunction it fails to find the associated value. I tried changing the Scope but that did not help.

User avatar
zEal
Dragon
Posts: 944
Joined: Sun Mar 22, 2009 2:25 am

Re: Custom "Robust" eval()

Post by zEal »

lmarkus001 wrote:Wait! The SLASH SLASH comment delimiter went live in b56? Woo hoo!
Nope.. sort of?

If you use the ignoreOutput parameter in the defineFunction() call you can then use what ever sort of comments you want.
lmarkus001 wrote:Hummm... this function is failing for calls that are structured like (assemble a variable name from two text strings):

Code: Select all

[H: jST = "[]"]
[H, FOREACH(st, rSubtypeList), CODE: {
  [R: temp = eval(st + "Subtype")]
  [IF(temp): jST = json.append(jST, st)]
}]
Where I have a bunch of variables defined for an input dialog that look like AirSubtype, AngelSubtype, AquaticSubtype, etc. So I want the eval() to find the value of the variable that is formed from assembling the two text strings.

If I feed this code to the real eval() it works fine, if fed to the improved evalFunction it fails to find the associated value. I tried changing the Scope but that did not help.
Good catch. Changing the newScope parameter does fix it though. I updated the first post.. I also updated all the variable names in the UDF because I'm very wary of accidentally changing a user's variables when using a newScope of false.

I imagine the reason it didn't seem to fix it for you is because you changed it, and then clicked onCamapaignLoad to redefine it.. but what that did was create a third definition of eval with an oldFunction pointing to the second definition of eval with an oldFunction pointing to the standard definition.

User avatar
lmarkus001
Great Wyrm
Posts: 1867
Joined: Sat Mar 29, 2008 12:30 am
Location: Layfayette Hill, PA

Re: Custom "Robust" eval()

Post by lmarkus001 »

Naw what I did was more bone-headed....

Code: Select all

[H: defineFunction("eval", "evalFunction@this"), 1, 0]

User avatar
zEal
Dragon
Posts: 944
Joined: Sun Mar 22, 2009 2:25 am

Re: Custom "Robust" eval()

Post by zEal »

lmarkus001 wrote:Naw what I did was more bone-headed....

Code: Select all

[H: defineFunction("eval", "evalFunction@this"), 1, 0]
Yeah, I wouldn't have guessed that was the problem. :)

I'm surprised that didn't cause onCampaignLoad to halt entirely.

User avatar
lmarkus001
Great Wyrm
Posts: 1867
Joined: Sat Mar 29, 2008 12:30 am
Location: Layfayette Hill, PA

Re: Custom "Robust" eval()

Post by lmarkus001 »

I found a failure point for this variant eval() and thought I should bring it to light so others don't struggle.

You can not use this custom eval() in a IF roll condition.

For example this fails:

Code: Select all

[C(10,""), IF( eval("Weap" + roll.count) == "" || eval("Weap" + roll.count) == 0), CODE:
  {
  };
  {
    [MACRO("LibAttack@Lib:libDnD35Pathfinder"): ("Weapon" +(eval("Weap" + roll.count) - 1))]
  }
]
but can be reworked:

Code: Select all

[C(10,""), CODE: {
    [H: tW = eval("Weap" + roll.count)]
    [H: doIt =  if( tW == "" || tW == 0, 0, 1)]
    [IF(doIt), CODE: {
      [MACRO("LibAttack@Lib:libDnD35Pathfinder"): ("Weapon" + ( tW - 1 ))] 
    }]
}]
But that will spam "" to the chat for the counts that doIt = 0.

User avatar
biodude
Dragon
Posts: 444
Joined: Sun Jun 15, 2008 2:40 pm
Location: Montréal, QC

Re: Custom "Robust" eval()

Post by biodude »

zEal wrote:A redefinition of the eval() function to work around its limitations.

Features this custom version has that the standard eval() lacks:
...
  • Returns empty strings instead of providing an error.
...
... Unless the preference for "Use ToolTips for Inline Rolls" is checked, in which case all the error messages come back :(

I think it has to do with the argument to suppress output when the function is defined, which is not working the same way when the ToolTip option is enabled, but I haven't found a workaround - any ideas?
"The trouble with communicating is believing you have achieved it"
[ d20 StatBlock Importer ] [ Batch Edit Macros ] [ Canned Speech UI ] [ Lib: Math ]

User avatar
biodude
Dragon
Posts: 444
Joined: Sun Jun 15, 2008 2:40 pm
Location: Montréal, QC

Re: Custom "Robust" eval()

Post by biodude »

biodude wrote:
zEal wrote:A redefinition of the eval() function to work around its limitations.

Features this custom version has that the standard eval() lacks:
...
  • Returns empty strings instead of providing an error.
...
... Unless the preference for "Use ToolTips for Inline Rolls" is checked, in which case all the error messages come back :(
I added a bit of string pattern matching to catch some obvious exceptions (strings that do not contain any alphanumeric characters at all):
evalRobust

Code: Select all

<!--
    Still throws errors if the preference "Use ToolTips for Inline Rolls" is enabled!
    I added some more error-checking to try to catch some of these other exceptions.
    - JAW/biodude 2010-07
-->
// Error handling
[ assert( argCount() >= 1, "<b>eval()</b> - Invalid number of parameters <i>0</i>,
expected at least <i>1</i> parameter.", 0 ) ]
 
// Initialise variables
[ X_Expression_X = arg( argCount()-1 ) ]    <!-- last argument! -->
[ X_CancelEval_X = 0 ]
[ X_TypeTest_X = json.type( X_Expression_X ) ]
 
// Handle all numbers
[ if( isNumber( X_Expression_X ) == 1 ), code:
{
    [ X_CancelEval_X = 1 ]
} ]
 
// Handle empty strings (& strings with no alphanumerics)
[ if( X_TypeTest_X == "UNKNOWN" ), code:
{
    [ if( X_Expression_X == "" || matches( X_Expression_X , "[^0-9a-zA-Z]+" ) ), code:
    {    <!-- string matching condition added by JAW/biodude 2010-07 -->
        [ X_CancelEval_X = 1 ]
    } ]
} ]
 
// Handle JSON types
[ if( X_TypeTest_X == "ARRAY" || X_TypeTest_X == "OBJECT" ), code:
{
    [ X_CancelEval_X = 1 ]
} ]

// Evaluate or cancel, then return
[ if( X_CancelEval_X == 1 ), code:
{
    [ macro.return = X_Expression_X ]
};{
    [ macro.return = oldFunction( X_Expression_X ) ]
} ]
 
"The trouble with communicating is believing you have achieved it"
[ d20 StatBlock Importer ] [ Batch Edit Macros ] [ Canned Speech UI ] [ Lib: Math ]

User avatar
aliasmask
RPTools Team
Posts: 9024
Joined: Tue Nov 10, 2009 6:11 pm
Location: Bay Area

Re: Custom "Robust" eval()

Post by aliasmask »

So, looking at your code, I'm guessing Wiki: oldFunction() is the same as using the UDF alias name? I've never used it before.

Instead of returning the JSON, maybe you can apply Wiki: json.evaluate() to it then return it... kind of a 2fer. Or not, because it's rarely used or needed.

User avatar
biodude
Dragon
Posts: 444
Joined: Sun Jun 15, 2008 2:40 pm
Location: Montréal, QC

Re: Custom "Robust" eval()

Post by biodude »

aliasmask wrote:So, looking at your code, I'm guessing Wiki: oldFunction() is the same as using the UDF alias name? I've never used it before.
Sort of: it calls the original function being replaced by the UDF. This is not the same as calling the function directly (now defined as a UDF), which would be recursive, as opposed to just calling the original function. I believe this is why oldFunction was created.
aliasmask wrote:Instead of returning the JSON, maybe you can apply Wiki: json.evaluate() to it then return it... kind of a 2fer. Or not, because it's rarely used or needed.
possibly - keep in mind this isn't really "my code", I just added a few tweaks to zEal's great work.
I kinda figured if you wanted json.evaluate, you'd just use that directly. The trouble with the original eval is that if you pass a string entered by the user, or pull one from a data source on a Token somewhere, you can't always be sure how that string is formatted. Furthermore, JSONs are not strings and cause problems for certain operations that expect one or the other. I suspect that's why the robust eval checks specifically for JSON structures: to avoid treating them like strings, which would otherwise throw errors when processed by Wiki: eval().

Still haven't figured out a way to suppress errors if the "Use ToolTips for Inline Rolls" option is enabled, but it seems the only way to do it currently is to check for as many exceptions as you can before calling the oldFunction ... *shrug*
"The trouble with communicating is believing you have achieved it"
[ d20 StatBlock Importer ] [ Batch Edit Macros ] [ Canned Speech UI ] [ Lib: Math ]

Post Reply

Return to “Macros”