macros:HowTo:howto: Difference between revisions

From RPTools Wiki
Jump to navigation Jump to search
Line 157: Line 157:


[H: macro.return = jAO]
[H: macro.return = jAO]
</source>
====Sorting JSON Objects Based on an Arbitrary Nested Value====
JSON Objects can contain an arbitrary number of keys and values (for which the value may itself be another JSON object). For example, a nested JSON object called "Monsters" might look like this:
<source lang="mtmacro" line>
{
"Troll":
  {
    "name":"Troll",
    "HD":4,
    "HP":75
  },
"Orc":
  {
    "name":"Orc",
    "HD":3,
    "HP":22
  }
}
</source>
Note that each value in the key-value pairs in the above object is actually a complete JSON object in its own right.
It is occasionally useful to be able to sort a JSON object that contains ''other'' JSON objects based on a value in one of the "sub-objects." For instance, if a JSON object exists that contains token names and distances to those tokens from a given point, one may want to sort the JSON so that the nearest objects are first, and the farthest are last.
Or, using the above "Monsters" example, one may wish to sort it by '''name''', or by '''HP''', or by '''HD'''.
The following macro routine is a generic method to sort JSON objects based on an arbitrary value within a nested object.
'''Assumptions'''
* This macro requires the use of a JSON object supporting version of MapTool; it was written and tested in version 1.3.b53.
* The macro is generic, and will run using any given JSON object conforming to the general "nested object" structure. In this case, a nested JSON object is created in the beginning so that there is a sample object to sort. '''However''': this routine should be applicable to JSON arrays of objects as well - it would simply require using the index of the nested object rather than the nested object's key.
* It can be unclear which object is being discussed when you have multiply nested objects. In the following explanation, "nested object" will always refer to an object contained ''within a larger JSON''. So, the larger object is '''Monsters''', while '''Troll''' would be a ''nested object.''
'''Macro Code and Discussion'''
Please see the [[Tutorials:Macros:JSONSortingFullCode|full macro code]] for the complete macro.
'''1. Create Sample Object'''
This sequence simply creates a sample object to practice sorting. In actual use, you may wish to pass an object as an argument, or pull an object from a token's properties, as necessary.
<source lang="mtmacro" line>
[h:troll = json.set("{}", "name", "Troll", "HD", 4, "HP", 75)]
[h:orc = json.set("{}", "name", "Orc", "HD", 3, "HP", 13)]
[h:goblin = json.set("{}", "name", "Goblin", "HD", 2, "HP", 6)]
[h:gnoll = json.set("{}", "name", "Gnoll", "HD", 3, "HP", 19)]
[h:kobold=json.set("{}", "name", "Kobold", "HD", 1, "HP", 4)]
[h:monsters = json.set("{}", "Troll", troll, "Orc", orc, "Goblin", goblin, "Gnoll", gnoll, "Kobold", kobold)]
</source>
'''2. Request Sorting Key and Sort Direction from User'''
This section is also optional (and not useful if this macro will be used as a function/called macro), but for the example code it makes it easier to experiment with. This section uses [[input|input()]] to gather user input, and [[abort|abort()]] to halt processing if the user hits "Cancel." Finally, it uses an [[Macros:Branching_and_Looping#IF_Option|IF():]] roll option to set a variable with a "friendly" indicator of sort direction, which will be used at the end in the final output.
<source lang="mtmacro" line>
[h:status = input(
"whichKey|name,HD,HP|Pick Sorting Key|LIST|SELECT=0 VALUE=STRING",
"whichDirection|A+,A-,N+,N-|Direction (A+/- for strings, N+/- for numbers!)|LIST|SELECT=0 VALUE=STRING"
)]
[h:abort(status)]
[h,if(substring(whichDirection,1)=="+"): dirString = "ascending"; dirString = "descending"]
</source>
'''3. Set Basic Variables'''
This segment initializes some variables that will be used later:
*''sortObj'' is the object to be sorted (in this case, the JSON Object '''Monsters''')
*''sortOn'' is the value on which to sort ('''name''', '''HD''', or '''HP''')
*''sortDirection'' is the direction of the sort, which will be passed to [[listSort|listSort()]]
*''sortObjContentList'' is a list - created using [[json.fields|json.fields()]] - of each nested object within '''Monsters'''; effectively it is a list of the "names" of each monster
*''keyList'' is a list that will contain the value that corresponds to the thing we're sorting on - so if you choose to sort by '''name''', then ''keyList'' will ultimately contain the value of '''name''' for each monster in the '''Monsters''' object
*''sortedJSON'' will hold the new, nicely sorted JSON object; the original object will be unchanged.
<source lang="mtmacro" line>
[h:sortObj=monsters]
[h:sortKey = whichKey]
[h:sortDirection = whichDirection]
[h:sortObjContentList = json.fields(sortObj)]
[h:keyList = ""]
[h:sortedJSON = "{}"]
</source>
'''4. Extract the Value of ''sortKey'' from each Nested Object'''
Here, we use FOREACH() to loop through each element in ''sortObjContentList'' (in other words, go one-by-one through the list of monster names). The FOREACH() option lets us say that ''item'' holds the value of each of those (so for the first pass, ''item'' holds the first monster name in the list, and on the second pass, it moves to the next, and so on). We need to do this so that we can extract the detailed information about each monsters from the '''Monsters''' object (in this case, we assign the detailed information to a new variable called ''itemDetail'').
With the nested objects extracted, we can then retrieve the value of the thing we're sorting on by using [[json.get|json.get()]] on the variable ''itemDetail''. We stick that value in the previously empty list ''keyList''.
Finally, once we've gone through each nested object held within '''Monsters''' and each nested object's value for our chosen sort (remember, we put that information in the variable ''sortKey'') has been added to ''keyList'', we're finished with the loop.
Now we actually can determine what the right order will ultimately be - we sort ''keyList'' using [[listSort|listSort()]] based on the direction specified by the user. '''This is a critical step!''' We've gone through each object, and figured out what the ''value'' of the thing we're sorting on is - so if we're sorting on "name", we've gone and actually retrieved each object's ''name'', and put it in a list with the others. We then sort that list, which tells us the final order to use when we reassemble the main object!
<source lang="mtmacro" line>
[h,foreach(item, sortObjContentList),CODE:
{
  [h:itemDetail = json.get(sortObj,item)]
  [h: keyList = listAppend(keyList, json.get(itemDetail, sortKey))]
}]
[h:keyList = listSort(keyList, sortDirection)]
</source>
'''5. Iterating through ''keyList'' and Each Nested Object'''
This is the most complex part of the routine. [[Macros:Branching_and_Looping#FOREACH_Option|FOREACH():]] through each element in the variable ''keyList'' (which, you will recall, contains the values corresponding to ''sortKey'' for each nested object). For ''each'' element in ''keyList'', we then loop through ''all'' of the nested objects in '''Monsters''' to see which one(s) match up to the current element of ''keyList''.
So, for example, if the current value - ''key'' - in the outer loop is 4, and we are sorting by "HD", the inner loop will iterate through each nested object and check to see if the value of "HD" for that nested object is equal to 4.
If a match is found, the matching nested object is added to ''sortedJSON'' using [[json.set|json.set()]]. In this fashion, we're using ''keyList'' to tell us what order the final nested objects should be in, and we then just need to go through our nested objects, setting them in that order via the following code.
<source lang="mtmacro" line>
[h,foreach(key,keyList),CODE:
{
  [foreach(object,sortObj),CODE:
  {
    [objectDetail = json.get(sortObj,object)]
    [h:sortOnValue = json.get(objectDetail, sortKey)]
    [if(sortOnValue == key): sortedJSON=json.set(sortedJSON, object, objectDetail);""]
  }]
}]
</source>
'''6. Outputting the Results in an Attractive Fashion'''
The final step is to output results. The use of [[json.indent|json.indent()]] here simply makes the sorted JSON object easy to read.
<source lang="mtmacro" line>
JSON Object sorted by [r:whichKey], [r:dirString]:<br>
<pre>[r:json.indent(sortedJSON, 3)]</pre>
</source>
</source>

Revision as of 16:15, 25 March 2009

Macro How To

How to automate updating a token property

This section expects that you are already familiar with how to add macro buttons to the MapTool user interface.

Example: Updating Hit Points

Let's say you have a property to represent hit points. We'll call our property HP. Now we want some easy way to update HP, so we're going to create a macro button that executes a macro.

First, consider how you want this to work. We want a window to popup on the screen and ask the user to enter a number. That number will be subtracted from HP, so the user can use a positive number to represent damage and a negative number to represent healing. (We'll show another approach later.)

The first step will be to prompt for the number. MapTool has this ability built-in. All we need to do is use a variable name that doesn't exist yet and MapTool will popup the prompt! The name of the variable is part of the prompt, so we'll use a descriptive name. How about AmountOfDamage?

[ damage = AmountOfDamage ]

Notice the extra variable name and the equals sign? That tells MapTool to calculate whatever is on the right of the equals sign and store the result into the variable on the left. In this case, there's no formula, so this becomes simply a copy -- from the variable AmountOfDamage to damage. But when MapTool tries to read the value of the variable and it doesn't exist, the popup will automatically appear! That's perfect for what we want!

Now the next step is to subtract that from the HP property. Fortunately, what you learned in the last paragraph can be used again:

[ damage = AmountOfDamage ]
[ HP = HP - damage ]

This time the second line calculates the formula on the right (HP - damage) and stores the result into ... HP? Isn't that going to screw up our HP value?

No, it doesn't screw it up, but it does replace that value with the result. And because HP is a property, the result is stored back into the token's property. If you were to right-click on the token and save it to an external file, the new value of HP is stored with it. When the token is later reloaded, that value will come with it.

If you want to add to the hit points instead, you have two choices: either the user can enter a negative number, or you can change the - to a +. The first option is easy because it puts the burden on the user! The second option is really an option -- who wants to edit their macro every time they want to switch from damage to healing? Another choice not listed above would be to create a second macro. Then there could be one macro for adding damage and one for adding healing.

There's a few things still needed here to make this a little prettier, but those are future steps. Go ahead and try this out right now on a token that you create in MapTool. (The default property type, Basic, includes a property named HP.) And try adding the second macro as well, just for the practice. (Believe me, the more practice you get early in the process, the easier it will become later on.)

Example: Let's Rest for a Minute...

So let's say that you now have a macro button that prompts you to change the token's hit points through damage or healing as described above. How do we reset their hit points to their maximum when they rest?

We already know that we have a HP and HPmax properties, so when they are healed up we simply need to copy the value in HPmax into HP. That should give you what you need to create a simple one-line macro:

[ HP = HPmax ]

Simple, right? But for the sake of argument, let's expand on this a bit. Instead of restoring all of the hit points to the creature, we will prompt the user for the number of hours that the creature will be resting. For my demonstration, I'm assuming that there's a property named Level. If it rests for less than 24 hours, it gets back Level*2 hit points. If it rests for 24 hours or more, it gets back Level*6.

[ hours = NumberOfHours ]
[ healing = if(hours < 24, Level * 2, Level * 6) ]
[ HP = HP + healing ]

You may notice the if() function on the second line. One word of warning when using the if() function: both the true and the false sections are executed! For that reason, you may want the IF() option instead. Note that the syntax is slightly different between the two, so be careful about which one you choose.

Example: One Macro to Rule Them All

((macro that uses input() to prompt for values with radiobuttons))

How to manipulate a JSON property

Example: jsonFilterArrObj() -- Filtering out objects from an array of objects

If you have an array of objects and want to filter that list given one of the elements in the object, this subroutine/callable macro jsonFilterArrObj will do the job.

As an example, here is an array of creature data:

JSON array of Creature objects

[{
    "name": "Umber Hulk",
    "hd": 8,
    "size": "Large",
    "reach": 10,
    "str": 23,
    "dex": 13,
    "con": 19,
    "mov": "20, burrow 20",
    "AC": "18/10/17",
    "ArmorClass": "Armor=0 ; Shield=0 ; ArmorACP=0 ; ShieldACP=0 ; MaxDex=50 ; Natural=8 ; Deflection=0 ; Dodge=0 ; Description= ;",
    "SpecialATK": "Confusing Gaze(Su)"
},
{

    "name": "Ogre",
    "hd": 4,
    "size": "Large",
    "reach": 10,
    "str": 21,
    "dex": 8,
    "con": 15,
    "mov": 30,
    "AC": "16/8/17 Hide",
    "ArmorClass": "Armor=3 ; Shield=0 ; ArmorACP=0 ; ShieldACP=0 ; MaxDex=50 ; Natural=5 ; Deflection=0 ; Dodge=0 ; Description=Hide ;",
    "SpecialATK": "NA"
}]


If you want to shorten the list by filtering on various elements of the object, you can call the jsonFilterArrObj macro to return the filtered array of objects.

To call the macro, setup the JSON parameter to pass like thus:

Calling Macro snippet (Remove all Creatures that have more HitDice than the entered number)

<!-- 
"hd" = Element I want to filter against for this example
jAll = JSON array of Creature objects
fHD = number I prompted for with an input() dialog, this is the value I am testing against
-->


[H: jAll = "[]"]
[H, FOR(i,1,cntAll): jAll = json.append(jALL, json.set(table("Polymorph", i), "imageID", tableimage("Polymorph", i)))]
[H: jAll = json.sort(jAll, "a")]

[H: tjF = json.append("[]", jAll)]
[H: tjF = json.append(tjF, "hd")]
[H: tjF = json.append(tjF, fHD)]
[H: tjF = json.append(tjF, "<")]
[H, MACRO("jsonFilterArrObj@"+getMacroLocation()): tjF]
[H: jFiltered = macro.return]
[H: Assert(!(json.isEmpty(jFiltered)), "Polymorph: No choices available.", 0)]
[H: cntF = json.length(jFiltered)]

<!-- continue processing with the newly filtered array of objects -->


jsonFilterArrObj

<!-- 
Filter out objects from an array of objects.
inputs (macro.args is a JSON array containing):
  jAO = A JSON array of objects
  elem = The element to filter against
  fDat = The data to compare against
  fType = Filter comparison: >, <, ==, >=, <=, !=
output:
  macro.return = jAOf (The array with the elements removed that met the criteria)
-->

[H: jAO = json.get(macro.args, 0)]
[H: elem = json.get(macro.args, 1)]
[H: fDat = json.get(macro.args, 2)]
[H: fType = json.get(macro.args, 3)]

[H: cntAll = json.length(jAO)]
[H: assert(cntAll, "Null Array so filter aborts.")]

[H, for(i, cntAll - 1, 0, -1), CODE: {
  [H: tDat = json.get(json.get(jAO, i), elem)]
  [ bTest = eval("fDat"+fType+"tDat")]<br>
  [H, IF(bTest): jAO = json.remove(jAO, i); ""]
}]

[H: macro.return = jAO]

Sorting JSON Objects Based on an Arbitrary Nested Value

JSON Objects can contain an arbitrary number of keys and values (for which the value may itself be another JSON object). For example, a nested JSON object called "Monsters" might look like this:

{
 "Troll":
  {
    "name":"Troll",
    "HD":4,
    "HP":75
  },
 "Orc":
  {
    "name":"Orc",
    "HD":3,
    "HP":22
  }
}

Note that each value in the key-value pairs in the above object is actually a complete JSON object in its own right.

It is occasionally useful to be able to sort a JSON object that contains other JSON objects based on a value in one of the "sub-objects." For instance, if a JSON object exists that contains token names and distances to those tokens from a given point, one may want to sort the JSON so that the nearest objects are first, and the farthest are last.

Or, using the above "Monsters" example, one may wish to sort it by name, or by HP, or by HD.

The following macro routine is a generic method to sort JSON objects based on an arbitrary value within a nested object.

Assumptions

  • This macro requires the use of a JSON object supporting version of MapTool; it was written and tested in version 1.3.b53.
  • The macro is generic, and will run using any given JSON object conforming to the general "nested object" structure. In this case, a nested JSON object is created in the beginning so that there is a sample object to sort. However: this routine should be applicable to JSON arrays of objects as well - it would simply require using the index of the nested object rather than the nested object's key.
  • It can be unclear which object is being discussed when you have multiply nested objects. In the following explanation, "nested object" will always refer to an object contained within a larger JSON. So, the larger object is Monsters, while Troll would be a nested object.

Macro Code and Discussion

Please see the full macro code for the complete macro.

1. Create Sample Object

This sequence simply creates a sample object to practice sorting. In actual use, you may wish to pass an object as an argument, or pull an object from a token's properties, as necessary.

[h:troll = json.set("{}", "name", "Troll", "HD", 4, "HP", 75)]
[h:orc = json.set("{}", "name", "Orc", "HD", 3, "HP", 13)]
[h:goblin = json.set("{}", "name", "Goblin", "HD", 2, "HP", 6)]
[h:gnoll = json.set("{}", "name", "Gnoll", "HD", 3, "HP", 19)]
[h:kobold=json.set("{}", "name", "Kobold", "HD", 1, "HP", 4)]
[h:monsters = json.set("{}", "Troll", troll, "Orc", orc, "Goblin", goblin, "Gnoll", gnoll, "Kobold", kobold)]

2. Request Sorting Key and Sort Direction from User

This section is also optional (and not useful if this macro will be used as a function/called macro), but for the example code it makes it easier to experiment with. This section uses input() to gather user input, and abort() to halt processing if the user hits "Cancel." Finally, it uses an IF(): roll option to set a variable with a "friendly" indicator of sort direction, which will be used at the end in the final output.

[h:status = input(
"whichKey|name,HD,HP|Pick Sorting Key|LIST|SELECT=0 VALUE=STRING",
"whichDirection|A+,A-,N+,N-|Direction (A+/- for strings, N+/- for numbers!)|LIST|SELECT=0 VALUE=STRING"
)]
[h:abort(status)]

[h,if(substring(whichDirection,1)=="+"): dirString = "ascending"; dirString = "descending"]

3. Set Basic Variables

This segment initializes some variables that will be used later:

  • sortObj is the object to be sorted (in this case, the JSON Object Monsters)
  • sortOn is the value on which to sort (name, HD, or HP)
  • sortDirection is the direction of the sort, which will be passed to listSort()
  • sortObjContentList is a list - created using json.fields() - of each nested object within Monsters; effectively it is a list of the "names" of each monster
  • keyList is a list that will contain the value that corresponds to the thing we're sorting on - so if you choose to sort by name, then keyList will ultimately contain the value of name for each monster in the Monsters object
  • sortedJSON will hold the new, nicely sorted JSON object; the original object will be unchanged.
[h:sortObj=monsters]
[h:sortKey = whichKey]
[h:sortDirection = whichDirection]
[h:sortObjContentList = json.fields(sortObj)]
[h:keyList = ""] 
[h:sortedJSON = "{}"]

4. Extract the Value of sortKey from each Nested Object

Here, we use FOREACH() to loop through each element in sortObjContentList (in other words, go one-by-one through the list of monster names). The FOREACH() option lets us say that item holds the value of each of those (so for the first pass, item holds the first monster name in the list, and on the second pass, it moves to the next, and so on). We need to do this so that we can extract the detailed information about each monsters from the Monsters object (in this case, we assign the detailed information to a new variable called itemDetail).

With the nested objects extracted, we can then retrieve the value of the thing we're sorting on by using json.get() on the variable itemDetail. We stick that value in the previously empty list keyList.

Finally, once we've gone through each nested object held within Monsters and each nested object's value for our chosen sort (remember, we put that information in the variable sortKey) has been added to keyList, we're finished with the loop.

Now we actually can determine what the right order will ultimately be - we sort keyList using listSort() based on the direction specified by the user. This is a critical step! We've gone through each object, and figured out what the value of the thing we're sorting on is - so if we're sorting on "name", we've gone and actually retrieved each object's name, and put it in a list with the others. We then sort that list, which tells us the final order to use when we reassemble the main object!

[h,foreach(item, sortObjContentList),CODE:
{
   [h:itemDetail = json.get(sortObj,item)]
   [h: keyList = listAppend(keyList, json.get(itemDetail, sortKey))]
}]

[h:keyList = listSort(keyList, sortDirection)]

5. Iterating through keyList and Each Nested Object

This is the most complex part of the routine. FOREACH(): through each element in the variable keyList (which, you will recall, contains the values corresponding to sortKey for each nested object). For each element in keyList, we then loop through all of the nested objects in Monsters to see which one(s) match up to the current element of keyList.

So, for example, if the current value - key - in the outer loop is 4, and we are sorting by "HD", the inner loop will iterate through each nested object and check to see if the value of "HD" for that nested object is equal to 4.

If a match is found, the matching nested object is added to sortedJSON using json.set(). In this fashion, we're using keyList to tell us what order the final nested objects should be in, and we then just need to go through our nested objects, setting them in that order via the following code.

[h,foreach(key,keyList),CODE:
{
   [foreach(object,sortObj),CODE:
   {
     [objectDetail = json.get(sortObj,object)]
     [h:sortOnValue = json.get(objectDetail, sortKey)]
     [if(sortOnValue == key): sortedJSON=json.set(sortedJSON, object, objectDetail);""]
   }]
}]

6. Outputting the Results in an Attractive Fashion

The final step is to output results. The use of json.indent() here simply makes the sorted JSON object easy to read.

JSON Object sorted by [r:whichKey], [r:dirString]:<br>
<pre>[r:json.indent(sortedJSON, 3)]</pre>